原始碼地址: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微服務實戰 》