服务间通信 Feign
一、服务间通信方式
服务间通信方式 主要有HTTP与RPC两种方式;而这两种通信方式的典型代表正是Spring Cloud全系列和阿里系Dubbo。Spring Cloud中服务间两种restful调用方式是RestTemplate和Feign.
1.1 RestTemplate的三种调用方式
1. 第一种方式
//在Controller中直接使用restTemplate,url写死
RestTemplate restTemplate = new RestTemplate();
String response = restTemplate.getForobject("http://locathost:8080/msg", String.class);
2. 第二种方式
//在Controller中利用loadBalancerClient通过应用名获取url,然后再使用 restTemplate
@Autowired
private LoadBalancerClient loadBalancerClient;
@Getmapping("/getProductMsg")
public String getProductMsg(){
RestTemplate restTemplate new RestTemplate();
Serviceinstance serviceinstance = loadBalancerClient.choose("PRODUCT");
string url = string.format("http: //%s:%s", serviceinstance.getHost(), serviceinstance.getPort())+"/msg";
string response = restTemplate.getforobject(url, string.class);
log.info("response=", response);
return response;
}
3. 第三种方式
@Component
public class RestTemplateconfig{
@bean
@LoadBalanced
public RestTemplate restTemplate(){
return new RestTemplate();
}
}
//在Controller中利用Loadbalanced,可在 resttemplate里使用应用名字
@Autowired
private RestTemplate restTemplate;
@Getmapping("/getProductMsg")
public String getProductMsg(){
String response = restTemplate.getforobject(url: "http://product/msg", String.class);
log.info("response=", response);
return response;
}
1.2 客户端软负载均衡器Ribbon
在Spring Cloud框架中,RestTemplate、Feign和网关zuul都默认使用了软负载均衡器Ribbon,即调用方式里集成了负载均衡Ribbon。
1. 负载均衡Ribbon的核心内容
- 服务发现
- 服务选择规则
- 服务监听
核心流程:ServerList获取所要的所有服务实例,ServerListFilter过滤掉一部分地址,IRule用来选取一个实例。
2. Ribbon的源码分析
在IDEA中利用编辑器自带功能观看源码的实现,以及类之间关系
默认负载均衡算法为轮询,可通过注解(@configuration 和 @bean结合使用)和配置的方法实现
PRODUCT:
ribbon:
NFLoadBalancerRuleClassName:com.nteflix.loadbalancer.RandomRule
二、Feign调用的代码实现
2.1 Feign的使用
实现与上述RestTemplate的三种调用方式起到相同作用的功能。
Feign的使用是 声明式REST客户端,采用了基于接口的注解
1. 主类OrderApplication上加注解@EnableFeignClients
<!--添加依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-feign</artifactId>
</dependency>
写一个ProductClient客户端
@FeignClient(name = "product")
public interface ProductClient { //无须实现,对开发者透明
@PostMapping("/msg") //匹配标识
String productMsg();
}
2. 在Controller中进行Feign的调用
@Autowired
private ProductClient productClient;
@Getmapping("/getProductMsg")
public String getProductMsg(){
String response = productClient.productMsg();
log.info("response=", response);
return response;
}
2.2 订单服务调用商品服务的代码实现
1. 获取商品列表(Feign)
在product服务中实现API
//在ProductServiceImpl中实现由productIdList获取productList
@Override
public List<ProductInfo> findList(List<String> productIdList) {
return productInfoRepository.findByProductIdIn(productIdList);
}
//在ProductController中实现给order服务feign调用的接口
@PostMapping("/listForOrder")
public List<ProductInfo> listForOrder(@RequestBody List<String> productIdList) {
return productService.findList(productIdList);
}
在order服务中实现feign调用
//在ProductClient客户端中声明ProductController中的接口
@PostMapping("/product/listForOrder")
List<ProductInfo> listForOrder(@RequestBody List<String> productIdList);
//在OrderController中实现feign调用
@PostMapping("/getProductList")
public String getProductList(){
List<ProductInfo> productInfoList = productClient.listForOrder(Arrays.asList("147283992738221"));
log.info("response=", productInfoList);
return "ok";
}
@RequestBody主要用来接收前端传递给后端的json字符串中的数据的(请求体中的数据的)
参数中出现@RequestBody 必须使用PostMapping,不能使用GetMapping,否则报错。
2. 扣库存(Feign)
在product服务中实现API
@Data
public class CartDTO {
// 商品id
private String productId;
//商品数量
private Integer productQuantity;
public CartDTO() { //无参构造
}
public CartDTO(String productId, Integer productQuantity) {
this.productId = productId;
this.productQuantity = productQuantity;
}
}
@Getter
public enum ResultEnum {
PRODUCT_NOT_EXIST(1, "商品不存在"),
PRODUCT_STOCK_ERROR(2, "库存有误"),
;
private Integer code;
private String message;
ResultEnum(Integer code, String message) {
this.code = code;
this.message = message;
}
}
//在ProductServiceImpl中实现cartDtoList减库存
@override
@Transactional
public void decreasestock(List<CartDto> cartDtoList){
for(CartDto cartDto:cartDtoList) {
Optional<ProductInfo> productInfoOptional = productInfoRepository.findbyId(cartDto.getProductId());
//商品是否存在
if(!productInfoOptional.ispresent()){
throw new ProductException(ResultEnum.PRODUCT_NOT_EXIST);
}
ProductInfo productInfo = productInfoOptional.get();
//库存是否足够
Integer result = productInfo.getProductStock()- cartDto.getProductQuantity();
if(result<0){
throw new ProductException(ResultEnum.PRODUCT_STOCK_ERROR);
}
productInfo.setProductStock(result);
productInfoRepository.save(productInfo);
}
}
//在ProductController中实现给order服务feign调用的接口
@PostMapping("/decreaseStock")
public void decreaseStock(@RequestBody List<CartDto> cartDtoList) {
productService.decreaseStock(cartDtoList);
}
在order服务中实现feign调用
//在ProductClient客户端中声明ProductController中的接口
@PostMapping("/product/decreaseStock")
void decreaseStock(@RequestBody List<CartDto> cartDtoList);
//在OrderController中实现feign调用
@PostMapping("/productDecreaseStock")
public String productDecreaseStock(){
productClient.decreaseStock(Arrays.asList(new CartDTO("147283992738221",3)));
return "ok";
}
3. 打通整个下单流程
@Service
public class OrderServiceImpl implements OrderService {
@Autowired
private OrderDetailRepository orderDetailRepository;
@Autowired
private OrderMasterRepository orderMasterRepository;
@Autowired
private ProductClient productClient;
@Override
public OrderDTO create(OrderDTO orderDTO) {
String orderId = KeyUtil.genUniqueKey();
//查询商品信息(调用商品服务)
List<String> productIdList = orderDTO.getOrderDetailList().stream()
.map(OrderDetail::getProductId)
.collect(Collectors.toList());
List<ProductInfoOutput> productInfoList = productClient.listForOrder(productIdList);
//计算总价
BigDecimal orderAmout = new BigDecimal(BigInteger.ZERO);
for (OrderDetail orderDetail: orderDTO.getOrderDetailList()) {
for (ProductInfoOutput productInfo: productInfoList) {
if (productInfo.getProductId().equals(orderDetail.getProductId())) {
//单价*数量
orderAmout = productInfo.getProductPrice()
.multiply(new BigDecimal(orderDetail.getProductQuantity()))
.add(orderAmout);
BeanUtils.copyProperties(productInfo, orderDetail);
orderDetail.setOrderId(orderId);
orderDetail.setDetailId(KeyUtil.genUniqueKey());
//订单详情入库
orderDetailRepository.save(orderDetail);
}
}
}
//扣库存(调用商品服务)
List<DecreaseStockInput> decreaseStockInputList = orderDTO.getOrderDetailList().stream()
.map(e -> new DecreaseStockInput(e.getProductId(), e.getProductQuantity()))
.collect(Collectors.toList());
productClient.decreaseStock(decreaseStockInputList);
//订单入库
OrderMaster orderMaster = new OrderMaster();
orderDTO.setOrderId(orderId);
BeanUtils.copyProperties(orderDTO, orderMaster);
orderMaster.setOrderAmount(orderAmout);
orderMaster.setOrderStatus(OrderStatusEnum.NEW.getCode());
orderMaster.setPayStatus(PayStatusEnum.WAIT.getCode());
orderMasterRepository.save(orderMaster);
return orderDTO;
}
}
4. 项目多模块改造
上述虽然完成了服务间的调用,但有一些不合理的地方。一是ProductClient客户端写在了order服务中,不符合系统低耦合的原则;二是Feign调用的时候,把商品服务自身的Entity对象(数据库表映射)直接暴露给了订单服务,不符合系统安全性。
4.1 服务分为多模块
将项目分为三个模块product-server、product-client、product-common三个模块,模块之间关系如下
- product-common模块存放公用的对象,如ProductInfoOutput对象(对ProductInfo做适度裁剪封装)、DecreaseStockInput(对CartDto做适度裁剪封装)
- product-server模块存放整个服务的业务逻辑
- product-client模块存放对外暴露的接口即ProductClient类
4.2 多模块改造实现
在项目根pom.xml下添加和改造
<modules>
<module>common</module>
<module>server</module>
<module>client</module>
</modules>
<packaging>pom</packaging> <!-- 由jar改为pom -->
<dependencyManagement>
<dependencies> <!-- 添加对product-common的依赖 -->
<dependency>
<groupId>com.imooc</groupId>
<artifactId>product-common</artifactId>
<version>${product-common.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
三、服务间通信之消息中间件
服务间通讯方式有同步和异步两种,上述两种restful调用方式RestTemplate和Feign都是同步消息,异步消息一般就是消息中间件;一般的消息中间件有RabbitMQ、Kafaka、ActiveMQ
同步消息方式的缺点是若一个服务需要调用多个服务来完成一个API接口的实现时,响应速度慢会有不好的用户体验,例如登录时,用户服务会调用短信服务、积分服务等,跳转时将会有一段时间的空白页面,造成用户体验不好。
异步消息即消息中间件可以很好的解决这个问题,逻辑图示如下
商品服务向消息队列发布库存变化(仅仅是商品ID和商品库存即CartDto即可),订单服务向消息队列订阅库存变化的消息(下单时即可访问自己本地的部分数据),然后发布扣库存的消息给消息队列,等待用户支付完成后,发布消息给消息队列,之后由消息队列发布给订阅过该消息积分服务和商品服务做相应操作,如此用不着等待用户支付完成才能真正同步完成下单的流程。
附(后续用到,暂不展开):
微服务MicroService与Docker、DevOps完美结合应用,MicroService是核心,Docker、DevOps是工具,解决自动部署等问题。
消息中间件RabbitMQ的安装也在Docker下进行。