一、OpenFeign

OpenFeign是Spring Cloud提供的一个声明式的伪Http客户端, 它使得调用远程服务就像调用本地服务一样简单, 只需要创建一个接口并添加一个注解即可。

Nacos很好的兼容了OpenFeign, OpenFeign默认集成了 Ribbon, 所以在Nacos下使用OpenFeign默认就实现了负载均衡的效果。

Feign是一个声明式的web服务客户端,让编写web服务客户端变得非常容易,只需创建一个接口并在接口上添加注解即可

1.1、作用

  • Feign使得编写Java Http客户端变得更加容易。
  • 在Feign的实现下,我们只需创建一个接口并使用注解的方式来配置它(以前是Dao接口上面标注Mapper注解,现在是一个微服务接口上面标注一个Feign注解即可),即可完成对服务提供方的接口绑定,简化了使用Spring cloud Ribbon时,自动封装服务调用客户端的开发量。
  • Feign集成了Ribbon利用Ribbon维护了Payment的服务列表信息,并且通过轮询实现了客户端的负载均衡。而与Ribbon不同的是,通过feign只需要定义服务绑定接口目以声明式的方法,优雅而简单的实现了服务调用。

1.2、Feign和OpenFeign

FeignOpenFeign
Feign是Spring Cloud组件中的一个轻量级RESTful的HTTP服务客户端Feign内置了Ribbon,用来做客户端负载均衡,去调用服务注册中心的服务。Feign的使用方式是:使用Feign的注解定义接口,调用这个接口,就可以调用服务注册中心的服务。OpenFeign是Spring Cloud 在Feign的基础上支持了Spring MVC的注解,如@RequesMapping等等。OpenFeign的@FeignClient可以解析Spring MVC的@RequestMapping注解下的接口,并通过动态代理的方式产生实现类,实现类中做负载均衡并调用其他服务。

1.3、OpenFeign使用步骤

1、创建微服务提供者集群

创建微服务模块cloud-provider-payment8001和cloud-provider-payment8002,payment微服务的控制器为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Slf4j
@RestController
@Api(tags = "支付功能控制器")
public class PaymentController {
@Resource
private PaymentService paymentService;
@Value("${server.port}")
private String serverPort;

@GetMapping("/payment/get/{id}")
@ApiOperation("根据id查询订单")
public CommonResult<Payment> getPaymentById(@PathVariable("id") Long id) {
Payment result = paymentService.getPaymentById(id);
if(result == null) {
return new CommonResult<>(404,"数据库中没有该订单!端口为:" + serverPort,null);
}
return new CommonResult<>(200,"成功!端口为:" + serverPort,result);
}
}

2、创建OpenFeign客户端

创建微服务模块cloud-consumer-feign-order80,这个微服务需要调用payment微服务中的接口。

注:OpenFeign是在消费端(微服务调用者端)使用的。

3、引入依赖

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
34
35
36
37
38
39
40
41
42
43
<!--openfeign-->
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>com.cloudstudy</groupId>
<artifactId>common</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>

<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

OpenFeign中自动集成了Ribbon,实现了负载均衡

image-20210211214254211

4、编写yml配置文件

1
2
3
4
5
6
7
8
server:
port: 80
eureka:
client:
# 不注册入eureka
register-with-eureka: false
service-url:
defaultZone: http://eureka7001.com:7001/eureka, http://eureka7002.com:7002/eureka

5、编写启动类,添加@EnableFeignClients注解激活OpenFeign

1
2
3
4
5
6
7
8
//激活并开启OpenFeign
@EnableFeignClients
@SpringBootApplication
public class OrderFeignMain80 {
public static void main(String[] args) {
SpringApplication.run(OrderFeignMain80.class);
}
}

6、编写一个Service接口,用于远程调用payment微服务

注意点:

  • 在Service接口上添加一个@FeignClient注解,value值为要调用的微服务在注册中心中注册的服务名;
  • 需要完整添加远程调用接口的全路径
1
2
3
4
5
@FeignClient(value = "cloud-payment-service")
public interface PaymentFeignService {
@GetMapping("/payment/get/{id}")
CommonResult<Payment> getPaymentById(@PathVariable("id") Long id);
}

7、在OpenFeign微服务中编写Controller

这个Controller调用上面写的OpenFeign服务接口

1
2
3
4
5
6
7
8
9
10
@RestController
public class OrderFeignController {
@Autowired
private PaymentFeignService paymentFeignService;

@GetMapping("/consumer/payment/get/{id}")
public CommonResult<Payment> getPaymentById(@PathVariable("id") Long id) {
return paymentFeignService.getPaymentById(id);
}
}

8、测试

启动eureka7001、payment8001、payment8002集群和openfeign微服务。

  • 由于openfeign微服务没有被我们注册入eureka,所以eureka注册中心显示如下

image-20210211220835349

  • 使用openfeign微服务远程调用payment微服务中的方法

image-20210211221340700

1.4、OpenFeign超时控制

1、OpenFeign默认等待时间

OpenFeign默认等待时间为1秒钟,若服务提供方超过这个时间则直接报错。

2、演示超时出错情况

在服务提供方payment微服务中新添一个接口,在接口中模拟超时情况

1
2
3
4
5
6
7
8
9
10
11
@GetMapping(value = "/payment/feign/timeout")
public String paymentFeignTimeOut() {
//在被调用的服务提供方中休眠三秒
//用于模拟长流程调用
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException interruptedException) {
interruptedException.printStackTrace();
}
return serverPort;
}

3、在OpenFeign微服务中添加远程调用接口

1
2
@GetMapping(value = "/payment/feign/timeout")
String paymentFeignTimeOut();

4、在OpenFeign微服务控制器中添加接口

1
2
3
4
@GetMapping(value = "/consumer/feign/timeout")
public String paymentFeignTimeOut() {
return paymentFeignService.paymentFeignTimeOut();
}

5、测试

先访问payment微服务中新增的接口

image-20210211222304721

测试远程调用payment微服务的超时接口

image-20210211222545603

6、设置OpenFeign的超时时间

修改yml配置文件

1
2
3
4
5
6
# 设置Feign客户端的超时时间(OpenFeign默认支持Ribbon)
ribbon:
#指的是建立连接后从服务器读取到可用资源所用的时间
ReadTimeout: 5000
#指的是建立连接所用的时间,适用于网络正常的情况下
ConnectTimeout: 5000

重启OpenFeign微服务,测试

image-20210211222811217

1.5、OpenFeign的日志打印功能

1、概述

Feign 提供了日志打印功能,我们可以通过配置来调整日志级别,从而了解 Feign中Http请求的细节。
说白了就是对Feign接口的调用情况进行监控和输出

2、级别说明

  • NONE:默认的,不显示任何日志;
  • BASIC:仅记录请求方法、URL、响应状态码及执行时间;
  • HEADERS:除了BASIC中定义的信息之外,还有请求和响应的头信息;
  • FULL:除了HEADERS中定义的信息之外,还有请求和响应的正文及元数据。

3、编写配置类,配置日志输出级别

1
2
3
4
5
6
7
8
9
10
11
@Configuration
public class FeignLogConfig {
/***
* 开启详细日志
* @return
*/
@Bean
Logger.Level feignLoggerLevel() {
return Logger.Level.FULL;
}
}

4、在yml文件中开启日志的Feign客户端

指定Feign以什么级别监控哪个接口

1
2
3
4
logging:
level:
# feign日志以什么级别监控哪个接口
com.hzx.springcloud.service.PaymentFeignService: debug

5、再次测试,查看日志输出

image-20210211223406662

日志输出情况

image-20210211223450652

二、在线教育项目整合OpenFeign

在线教育中,我们创建了两个用于与阿里云交互的微服务,分别为用于文件上传的oss微服务和用于视频点播的vod微服务,在其他微服务中,我们需要远程调用这两个微服务中的接口来完成业务需求。

2.1、远程调用OSS微服务

在删除讲师信息时,我们希望一同删除该讲师上传至阿里云中的头像文件,该过程中需要远程调用oss微服务中的文件删除接口

1、Oss微服务中的文件删除接口

  • Controller层
1
2
3
4
5
6
@DeleteMapping("remove")
@ApiOperation("图片文件删除")
public R removeFile(@RequestBody String url) {
fileService.removeFile(url);
return R.ok().message("文件删除成功!");
}
  • Service实现类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Override
public void removeFile(String url) {
//读取配置信息
String endpoint = ossProperties.getEndpoint();
String keyid = ossProperties.getKeyid();
String keysecret = ossProperties.getKeysecret();
String bucketname = ossProperties.getBucketname();
//创建OSSClient实例
OSS ossClient = new OSSClientBuilder().build(endpoint,keyid,keysecret);
//获取要删除的文件名,根据url获取
//主机名
String host = "https://" + bucketname + "." + endpoint + "/";
//从主机名长度的下标开始截取,就可以获取文件名
String objectName = url.substring(host.length());
System.out.println(objectName);
ossClient.deleteObject(bucketname,objectName);
//关闭OSSClient
ossClient.shutdown();
}

2、在edu微服务中远程调用Oss微服务

在edu微服务中创建一个feign包,包下存放远程调用接口

1
2
3
4
5
6
7
8
9
10
11
@Service
@FeignClient(value = "service-oss")
public interface OssFileService {
/***
* 远程调用OSS微服务中的控制器方法
* @param url
* @return
*/
@DeleteMapping("/admin/oss/file/remove")
R removeFile(@RequestBody String url);
}

在edu微服务中调用feign接口删除讲师头像

teacherService.removeAvatarById(id);

1
2
3
4
5
6
7
8
9
10
11
12
13
/***
* 根据id删除讲师
* @param id 要删除的讲师id
* @return 删除结果及提示信息
*/
@DeleteMapping("remove/{id}")
@ApiOperation(value = "根据ID删除讲师", notes = "根据ID删除讲师")
public R removeById(@ApiParam(value = "讲师id",required = true) @PathVariable("id") String id) {
//删除讲师前先删除讲师头像
teacherService.removeAvatarById(id);
boolean flag = teacherService.removeById(id);
return flag ? R.ok().message("删除成功!") : R.error().message("删除失败!");
}

removeAvatarById(id)方法如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Service
public class TeacherServiceImpl extends ServiceImpl<TeacherMapper, Teacher> implements TeacherService {
/***
* 注入远程调用接口
*/
@Autowired
private OssFileService ossFileService;
@Override
public boolean removeAvatarById(String id) {
//1 根据id获取讲师avatar的url
Teacher teacher = baseMapper.selectById(id);
if(teacher != null) {
String avatarUrl = teacher.getAvatar();
if(StringUtils.isNotBlank(avatarUrl)) {
//头像不为空才进行远程调用
R r = ossFileService.removeFile(avatarUrl);
return r.getSuccess();
}
}
return false;
}
}