源码地址:https://gitee.com/fighter3/eshop-project.git

持续更新中……

在上一个章节,我们已经成功地将服务注册到了Nacos注册中心,实现了服务注册和服务发现,接下来我们要做的是服务间调用。

想一下,我们日常调用接口有哪些方式呢?常见有的有JDK自带的网络连接类HttpURLConnection、Apache Common封装的HttpClient、Spring封装的RestTemplate。这些调用接口工具也许在你看来都并不困难那,但是如果引入feign,使用声明式调用,调用远程服务像调用本地api一样丝滑。

OpenFeign项目地址:https://github.com/OpenFeign/feign

1、Feign简介

Feign是一种声明式、模板化的HTTP客户端。使用Feign,可以做到声明式调用。

尽管Feign目前已经不再迭代,处于维护状态,但是Feign仍然是目前使用最广泛的远程调用框架之一。

在SpringCloud Alibaba的生态体系内,有另一个应用广泛的远程服务调用框架Dubbo,在后面我们会接触到。

Feign是在RestTemplate 和 Ribbon的基础上进一步封装,使用RestTemplate实现Http调用,使用Ribbon实现负载均衡。

接下来,我们开始学习Feign的使用,非常简单!

2、Feign使用

2.1、引入OpenFeign

在前面的章节里,我们已经引入了SpringCloud,现在我们只需要在需要引入的子模块中添加依赖:

        <dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

2.2、Feign远程调用

我们现在来完成一个业务:添加商品

这个业务涉及两个子服务,添加商品的时候同时要添加库存,查询商品的时候,同时要查询库存。商品服务作为消费者,库存服务作为生产者。

2.2.1、服务提供者

作为服务提供者的库存服务很简单,提供两个接口添加库存根据商品ID获取库存量

  • 控制层
@RestController
@RequestMapping("/shop-stock/api")
@Slf4j
@Api(value = "商品服务对外接口", tags = "商品服务对外接口")
public class ShopStockApiController {
@Autowired
private IShopStockService shopStockService; @PostMapping(value = "/add")
@ApiOperation("添加库存")
public Integer addStock(@RequestBody StockAddDTO stockAddDTO) {
log.info("client call add stock interface,param:{}", stockAddDTO);
return this.shopStockService.addStockApi(stockAddDTO);
} @GetMapping(value = "/account/get")
@ApiOperation("根据商品ID获取库存量")
public Integer getAccountById(@RequestParam Integer goodsId) {
return this.shopStockService.getAccountById(goodsId);
}
}

注意看,为了演示出本地调用类似的效果,这两个接口和普通的前后端接口不同。

我们没有返回之前定下的统一返回结果CommonResult,而是直接返回了数据。

  • 业务层

    普通的增、查而已

    /**
* 添加库存-直接返回主键
*
* @param stockAddDTO
* @return
*/
public Integer addStockApi(StockAddDTO stockAddDTO) {
ShopStock stock = new ShopStock();
stock.setGoodsId(stockAddDTO.getGoodsId());
stock.setInventory(stockAddDTO.getAccount());
log.info("准备添加库存,参数:{}", stock.toString());
this.baseMapper.insert(stock);
Integer stockId =stock.getStockId();
log.info("添加库存成功,stockId:{}", stockId);
return stockId;
} /**
* 根据商品ID获取商品库存
*
* @param goodsId
* @return
*/
public Integer getAccountById(Integer goodsId) {
ShopStock stock = this.getOne(Wrappers.<ShopStock>lambdaQuery().eq(ShopStock::getGoodsId, goodsId));
Integer account = stock.getInventory();
return account;
}
  • 添加库存实体类
@Data
@EqualsAndHashCode(callSuper = false)
@ApiModel(value = "库存添加", description = "")
public class StockAddDTO implements Serializable {
private static final long serialVersionUID = 1L; @ApiModelProperty(value = "商品主键")
private Integer goodsId; @ApiModelProperty(value = "数量")
private Integer account;
}

至此,我们的服务提供者的相关开发到此完成,打开地址 http://localhost:8050/doc.html ,可以看到我们开发的接口:

2.2.2、服务消费者

好了,接下里要开始我们的服务消费者,也就是商品服务的开发。

  • 远程调用Feign客户端

声明式调用——看一下Feign客户端的代码,你就知道什么是声明式调用:

/**
* @Author: 三分恶
* @Date: 2021/5/26
* @Description: 库存服务feign客户端
**/
@FeignClient(value = "stock-service")
public interface StockClientFeign { /**
* 调用添加库存接口
*
* @param stockAddDTO
* @return
*/
@PostMapping(value = "/shop-stock/api/add")
Integer addStock(@RequestBody StockAddDTO stockAddDTO); /**
* 调用根据商品ID获取库存量接口
*
* @param goodsId
* @return
*/
@GetMapping(value = "/shop-stock/api/account/get")
Integer getAccountById(@RequestParam(value = "goodsId") Integer goodsId);
}
  • 定义完成之后,我们还要在启动类上加上注解@EnableFeignClients去扫描Feign客户端。
@SpringBootApplication
@MapperScan("cn.fighter3.mapper")
@EnableDiscoveryClient
@EnableFeignClients(basePackages = "cn.fighter3.client")
public class EshopGoodsApplication { public static void main(String[] args) {
SpringApplication.run(EshopGoodsApplication.class, args);
}
}

使用Feign客户端也很简单,直接在需要使用的地方注入就行了。

@Autowired
private StockClientFeign stockClientFeign;
  • 商品服务控制层
/**
* <p>
* 前端控制器
* </p>
*
* @author 三分恶
* @since 2021-05-18
*/
@RestController
@RequestMapping("/shop-goods")
@Api(value = "商品管理接口", tags = "商品接口")
@Slf4j
public class ShopGoodsController { @Autowired
private IShopGoodsService goodsService; @PostMapping(value = "/add")
@ApiOperation(value = "添加商品")
public CommonResult addGoods(@RequestBody GoodsAddDTO goodsAddDTO) {
return this.goodsService.addGoods(goodsAddDTO);
} @GetMapping(value = "/get/by-id")
@ApiOperation(value = "根据ID获取商品")
public CommonResult<GoodsVO> getGoodsById(@RequestParam Integer goodsId) {
return this.goodsService.getGoodsById(goodsId);
} }
  • 服务层

在服务层除了对商品库的操作之外,还通过Feign客户端远程调用库存服务的接口。

@Service
@Slf4j
public class ShopGoodsServiceImpl extends ServiceImpl<ShopGoodsMapper, ShopGoods> implements IShopGoodsService { @Autowired
private StockClientFeign stockClientFeign; /**
* 添加商品
*
* @param goodsAddDTO
* @return
*/
public CommonResult addGoods(GoodsAddDTO goodsAddDTO) {
ShopGoods shopGoods = new ShopGoods();
BeanUtils.copyProperties(goodsAddDTO, shopGoods);
this.baseMapper.insert(shopGoods);
log.info("添加商品,商品主键:{}", shopGoods.getGoodsId());
log.info(shopGoods.toString());
StockAddDTO stockAddDTO = StockAddDTO.builder().goodsId(shopGoods.getGoodsId()).account(goodsAddDTO.getAccount()).build();
log.info("准备添加库存,参数:{}", stockAddDTO.toString());
Integer stockId = this.stockClientFeign.addStock(stockAddDTO);
log.info("添加库存结束,库存主键:{}", stockId);
return CommonResult.ok();
} /**
* 获取商品
*
* @param goodsId
* @return
*/
public CommonResult<GoodsVO> getGoodsById(Integer goodsId) {
GoodsVO goodsVO = new GoodsVO();
//获取商品基本信息
ShopGoods shopGoods = this.baseMapper.selectById(goodsId);
BeanUtils.copyProperties(shopGoods, goodsVO);
//获取商品库存数量
Integer account = this.stockClientFeign.getAccountById(goodsId);
log.info("商品数量:{}", account);
goodsVO.setAccount(account);
return CommonResult.ok(goodsVO);
}
}
  • 实体类

    添加库存实体类和库存服务相同,略过,商品展示实体类

@Data
@EqualsAndHashCode(callSuper = false)
@ApiModel(value = "商品", description = "")
public class GoodsVO implements Serializable {
private static final long serialVersionUID = 1L; @ApiModelProperty(value = "商品主键")
private Integer goodsId; @ApiModelProperty(value = "商品名称")
private String goodsName; @ApiModelProperty(value = "价格")
private BigDecimal price; @ApiModelProperty(value = "商品介绍")
private String description; @ApiModelProperty(value = "数量")
private Integer account;
}

2.2.3、效果演示

接下来启动nacos-server,商品服务,库存服务。

访问地址 http://127.0.0.1:8848/nacos/index.html ,登录之后,可以在服务列表里看到我们注册的两个服务:

访问商品服务Knife4j地址:http://localhost:8020/doc.html ,可以看到添加商品和根据商品ID查找商品的接口,分别调试调用:

  • 添加商品

  • 根据ID获取商品

可以看到各自对应的数据库也有数据生成:

整体的远程调用示意图大概如下:

2.3、Ribbon负载均衡

关于负载均衡,这里偷个懒,就不再演示了。

感兴趣的可以吧库存服务打包,以不同的端口启动,然后添加商品,通过日志查看商品服务调用的负载情况。

Feign负载均衡是通过Ribbon实现,Ribbon是一种客户端的负载均衡——也就是从注册中心获取服务列表,由客户端自己决定调用哪一个远程服务。

Ribbon的主要负载均衡策略有以下几种:

规则名称 特点
AvailabilityFilteringRule 过滤掉一直连接失败的被标记为circuit tripped的后端Server,并 过滤掉那些高并发的后端Server或者使用一个AvailabilityPredicate 来包含过滤server的逻辑,其实就是检查status里记录的各个server 的运行状态
BestAvailableRule 选择一个最小的并发请求的server,逐个考察server, 如果Server被tripped了,则跳过
RandomRule 随机选择一个Server
ResponseTimeWeightedRule 已废弃,作用同WeightedResponseTimeRule
WeightedResponseTimeRule 根据响应时间加权,响应时间越长,权重越小,被选中的可能性越低
RetryRule 对选定的负载均衡策略加上重试机制,在一个配置时间段内当 选择Server不成功,则一直尝试使用subRule的方式选择一个 可用的Server
RoundRobinRule 轮询选择,轮询index,选择index对应位置的Server
ZoneAvoidanceRule 默认的负载均衡策略,即复合判断Server所在区域的性能和Server的可用性 选择Server,在没有区域的环境下,类似于轮询(RandomRule)

这里就不再展开讲了,感兴趣的自行了解。

3、意外状况

  • 发现远程调用的时候出现读取响应结果超时的情况:
java.net.SocketTimeoutException: Read timed out

修改Ribbon超时配置就行了:

# ribbon超时时间
ribbon:
ReadTimeout: 30000
ConnectTimeout: 30000
  • Feign接口中,使用@RequestParam报错

发现报错:

Caused by: java.lang.IllegalStateException: RequestParam.value() was empty on parameter 0

Feign声明里需要加上value

Integer getAccountById(@RequestParam(value = "goodsId") Integer goodsId);

"简单的事情重复做,重复的事情认真做,认真的事情有创造性地做!"——

我是三分恶,可以叫我老三/三分/三哥/三子,一个能文能武的全栈开发,咱们下期见!


参考:

【1】:小专栏《SpringCloudAlibaba微服务实战 》

【2】:SpringCloud Alibaba微服务实战三 - 服务调用

【3】:Ribbon的负载均衡策略、原理和扩展

SpringCloud Alibaba实战(8:使用OpenFeign服务调用)的更多相关文章

  1. SpringCloud Alibaba微服务实战三 - 服务调用

    导读:通过前面两篇文章我们准备好了微服务的基础环境并让accout-service 和 product-service对外提供了增删改查的能力,本篇我们的内容是让order-service作为消费者远 ...

  2. SpringCloud Alibaba实战(11:引入服务网关Gateway)

    源码地址:https://gitee.com/fighter3/eshop-project.git 持续更新中-- 大家好,我是三分恶. 在前面的章节中,我们已经完成了服务间的调用.统一配置等等,在这 ...

  3. SpringCloud Alibaba实战(12:引入Dubbo实现RPC调用)

    源码地址:https://gitee.com/fighter3/eshop-project.git 持续更新中-- 大家好,我是老三,断更了半年,我又滚回来继续写这个系列了,还有人看吗-- 在前面的章 ...

  4. SpringCloud Alibaba实战(6:nacos-server服务搭建)

    源码地址:https://gitee.com/fighter3/eshop-project.git 持续更新中-- 大家好,我是三分恶. 这一节我们来学习SpringCloud Alibaba体系中一 ...

  5. SpringCloud Alibaba实战(7:nacos注册中心管理微服务)

    源码地址:https://gitee.com/fighter3/eshop-project.git 持续更新中-- 在上一节我们已经完成了Nacos Server的本地部署,这一节我们学习如何将Nac ...

  6. SpringCloud实战-Feign声明式服务调用

    在前面的文章中可以发现当我们通过RestTemplate调用其它服务的API时,所需要的参数须在请求的URL中进行拼接,如果参数少的话或许我们还可以忍受,一旦有多个参数的话,这时拼接请求字符串就会效率 ...

  7. SpringCloud Alibaba实战(9:Hystrix容错保护)

    源码地址:https://gitee.com/fighter3/eshop-project.git 持续更新中-- 在上一节我们已经使用OpenFeign完成了服务间的调用.想一下,假如我们一个服务链 ...

  8. SpringCloud Alibaba实战(2:电商系统业务分析)

    选用了很常见的电商业务来进行SpringCloud Alibaba的实战. 当然,因为仅仅是为了学习SpringCloud Alibaba,所以对业务进行了大幅度简化,这里只取一个精简版的用户下单业务 ...

  9. SpringCloud Alibaba实战(3:存储设计与基础架构设计)

    1.存储设计 在上一章中,我们已经完成了基本业务流程的梳理和服务模块的划分,接下来,开始设计数据存储. 虽然在微服务的理论中,没有对数据库定强制性的规范,但一般,服务拆分之后,数据库也会对应的拆分. ...

随机推荐

  1. QFNU-11.08training

    7-1  阅览室 题目: 天梯图书阅览室请你编写一个简单的图书借阅统计程序.当读者借书时,管理员输入书号并按下S键,程序开始计时:当读者还书时,管理员输入书号并按下E键,程序结束计时.书号为不超过10 ...

  2. CMMI V2.0丨如何通过CMMI真正在企业中的实施规模化敏捷开发

    在过去的几年中,敏捷开发已经从一个利基概念(利基是指针对企业的优势细分出来的市场,这个市场不大,而且没有得到令人满意的服务.产品推进这个市场,有盈利的基础.)转变为全球许多大公司采用的标准实践. 通过 ...

  3. 简单了解 MySQL 中相关的锁

    本文主要是带大家快速了解 InnoDB 中锁相关的知识 为什么需要加锁 首先,为什么要加锁?我想我不用多说了,想象接下来的场景你就能 GET 了. 你在商场的卫生间上厕所,此时你一定会做的操作是啥?锁 ...

  4. [bug] SSM项目:Cannot load driver class: com.mysql.jdbc.Driver

    检查pom文件,mysql包部分为: <dependency> <groupId>mysql</groupId> <artifactId>mysql-c ...

  5. [DB] ElasticSearch

    安装 root用户解压,修改配置文件 创建新用户es 修改文件权限:chown -R es:es /kkb/install/elasticsearch-6.7.0/ 用es用户启动ElasticSea ...

  6. [bug] MapReduce卡死

    参考 https://blog.csdn.net/WYpersist/article/details/80202055

  7. [刷题] PTA 02-线性结构1 两个有序链表序列的合并

    程序: 1 #include <stdio.h> 2 #include <stdlib.h> 3 4 typedef int ElementType; 5 typedef st ...

  8. VS 中的 lib 和 dll 的区别和使用

    在 vs/c# 项目开发中,经常会遇到 lib 和 dll 文件,而且创建工程项目以及工程项目打包时也是必须要面对的,所以有必要掌握 lib 和 dll 的区别和使用. 静态库:在链接步骤中,连接器将 ...

  9. TB6560步进电机驱动板

    极客工坊比较好的帖子: 关于驱动板的共阴极和共阳极接法 http://www.geek-workshop.com/thread-12695-1-1.html

  10. python使用多线程备份数据库

    前言:在日常服务器运维工作中,备份数据库是必不可少的,刚工作那会看到公司都是用shell脚本循环备份数据库,到现在自己学习python语言后,利用多进程多线程相关技术来实现并行备份数据库,充分利用服务 ...