Spring Cloud实战-03订单微服务与商品微服务之间的调用

Spring Cloud【Finchley】专栏

如果还没有系统的学过Spring Cloud ,先到我的专栏去逛逛吧

Spring Cloud 【Finchley】手札


概述

还记得上篇博文的TODO吧

Spring Cloud实战-03订单微服务与商品微服务之间的调用

这里我们先循序渐进的了解下,微服务之间调用的几种方式

先了解下应用之间的通行的主要两种方式

  • RPC – 代表 Dubbo (可以基于TCP协议,也可以基于HTTP协议)
  • HTTP —代表 Spring Cloud (基于HTTP协议)

HTTP方式之RestTemplate

我们在order微服务调用product微服务。

product作为服务端,先对外暴露个测试接口
Spring Cloud实战-03订单微服务与商品微服务之间的调用

order作为客户端调用该接口

Spring Cloud实战-03订单微服务与商品微服务之间的调用

方式一 (直接使用restTemplate访问URL,url写死)

Spring Cloud实战-03订单微服务与商品微服务之间的调用

访问 http://localhost:8081/order/getServerInfoFromClient

Spring Cloud实战-03订单微服务与商品微服务之间的调用

写死的地址,并且只能请求一个,如果有多个地址就比较麻烦了。而且还是IP地址。


方式二 (使用LoadBalancerClient通过应用名获取url,拼装请求地址,然后再使用restTemplate)

package com.artisan.order.controller;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

@RestController
@Slf4j
@RequestMapping("/order")
public class ClientController {


    @Autowired
    private LoadBalancerClient loadBalancerClient;

    @GetMapping("/getServerInfoFromClient")
    public String requestServer(){


        // 方式二 (使用LoadBalancerClient通过应用名获取url,拼装请求地址,然后再使用restTemplate)
        RestTemplate restTemplate2 = new RestTemplate();
        ServiceInstance serviceInstance = loadBalancerClient.choose("ARTISAN-PRODUCT");
        // 获取ip port 组装url
        String url = String.format("http://%s:%s",
                serviceInstance.getHost(),serviceInstance.getPort() + "/product/serverMsg");
        log.info("url:{}",url);

        String msg = restTemplate2.getForObject(url,String.class);
        log.info("msg from server : {}", msg);
        return msg;
    }
}

loadBalancerClient.choose("ARTISAN-PRODUCT"); 通过loadBalancerClient 选择 注册到Eurek Server上的ip . 需要填写注册到注册中心的名字ARTISAN-PRODUCT。

访问 http://localhost:8081/order/getServerInfoFromClient

Spring Cloud实战-03订单微服务与商品微服务之间的调用


方式三 (使用@LoadBalanced注解)

先初始化RestTemplate , 标注 @LoadBalanced 注解

package com.artisan.order.config;

import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;

@Configuration
public class RestTemplateConfig {

    @Bean
    @LoadBalanced
    public RestTemplate restTemplate(){
        return new RestTemplate();
    }
}
package com.artisan.order.controller;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

@RestController
@Slf4j
@RequestMapping("/order")
public class ClientController {

    @Autowired
    private RestTemplate restTemplate;

    @GetMapping("/getServerInfoFromClient")
    public String requestServer(){

        // 方式三  (使用@LoadBalanced注解)

        String msg = restTemplate.getForObject("http://ARTISAN-PRODUCT/product/serverMsg",String.class);

        log.info("msg from server : {}", msg);
        return msg;
    }
}

请求的地址 http://ARTISAN-PRODUCT/product/serverMsg 注册到服务中心上的服务

访问 http://localhost:8081/order/getServerInfoFromClient

Spring Cloud实战-03订单微服务与商品微服务之间的调用


Fegin 的使用

Spring Cloud【Finchley】-06服务消费者整合Feign

总体来说,在作为客户端的order微服务中, 步骤如下

  1. 添加依赖
  2. 添加注解@EnableFeignClients
  3. 开发接口
  4. 使用

pom.xml 添加依赖

Spring Cloud实战-03订单微服务与商品微服务之间的调用


添加注解@EnableFeignClients
Spring Cloud实战-03订单微服务与商品微服务之间的调用


编写client接口

package com.artisan.order.client;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;

// name为注册在注册中心上的名称
@FeignClient(name="ARTISAN-PRODUCT")
public interface ProductClient {

    // ARTISAN-PRODUCT微服务接口的访问路径
    @GetMapping("/product/serverMsg")
    String getServerInfo();
}

调用

package com.artisan.order.controller;

import com.artisan.order.client.ProductClient;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@Slf4j
@RequestMapping("/order")
public class FeginClientController {

    @Autowired
    private ProductClient productClient;

    @GetMapping("/getServerInfoByFeign")
    public String requestServer(){

        String msg = productClient.getServerInfo();

        log.info("msg from server : {}", msg);
        return msg;
    }
}

访问 http://localhost:8081/order/getServerInfoByFeign

Spring Cloud实战-03订单微服务与商品微服务之间的调用

OK。


商品微服务获取商品列表功能开发

熟悉了基本使用后,刚开始说了,我们有几个TODO要做,那开始吧


Product微服务查询商品列表功能开发

我们看下前台会传递什么给我们
Spring Cloud实战-03订单微服务与商品微服务之间的调用

关于商品的信息,productId是个集合,那么我们就需要提供一个根据传入的productId列表来返回product集合的功能。

DAO层

老规矩,DAO层先

Spring Cloud实战-03订单微服务与商品微服务之间的调用
单元测试下,

Spring Cloud实战-03订单微服务与商品微服务之间的调用

   @Test
    public void findByProductIdIn() {
        List<Product> list =  productRepository.findByProductIdIn(Arrays.asList("1","2"));

        Assert.assertEquals(2,list.size());

    }

结合库表中的数据
Spring Cloud实战-03订单微服务与商品微服务之间的调用
Spring Cloud实战-03订单微服务与商品微服务之间的调用
单元测试通过


Service层

紧接着Service层

Spring Cloud实战-03订单微服务与商品微服务之间的调用

实现类
Spring Cloud实战-03订单微服务与商品微服务之间的调用

单元测试

Spring Cloud实战-03订单微服务与商品微服务之间的调用
单元测试通过

Spring Cloud实战-03订单微服务与商品微服务之间的调用


Controller层

  /**
     * 根据productIdList 查询商品列表
     * 提供给Order微服务用
     * @param productIdList
     * @return
     */
    @PostMapping("/productListForOrder")
    private List<Product> getProductForOrder(@RequestBody  List<String> productIdList){
        return productService.getProductList(productIdList);
    }

Order微服务调用接口查询商品列表

增加接口方法

Spring Cloud实战-03订单微服务与商品微服务之间的调用

返回的类型是个Product集合,我们先从product微服务那边将Product copy一份过来。 后续会优化这些地方

我们写个方法来测试下这个功能, 那就在刚才的用作测试的FeginClientController类中写个方法吧

Spring Cloud实战-03订单微服务与商品微服务之间的调用

当参数中标注了@RequestBody , 则必须使用POST方法

启动服务,测试下 http://localhost:8081/order/getProductList

Spring Cloud实战-03订单微服务与商品微服务之间的调用
可见功能是OK的。


调用商品微服务扣库存功能开发

Product微服务减库存功能开发

减库存的参数 DTO封装

我们看下前台会传递什么给我们

Spring Cloud实战-03订单微服务与商品微服务之间的调用

肯定是 某个产品 扣除多少个数量。 []可以传递多个,对于后台来讲是个集合 。

Product微服务需要两个参数 productId 和 productQuantity

Spring Cloud实战-03订单微服务与商品微服务之间的调用


Service

分析下,扣减库存,直接使用JPA内置的方法即可,DAO层可以省略了。

直接来Service吧 ,直接写实现类中的方法你把

@Override
    // 因为是对List操作,所以加个事务控制
    @Transactional
    public void decreaseProduct(List<CartDTO> cartDTOList) {

        // 遍历CartDTO
        for (CartDTO cartDTO : cartDTOList) {
            // 根据productId查询Product
            Optional<Product> productOptional = productRepository.findById(cartDTO.getProductId());

            // 商品是否存在
            if (!productOptional.isPresent()) {
                throw new ProductException(ResultEnum.PRODUCT_NOT_EXIST);
            }
            // 是否库存充足

            Product product = productOptional.get();
            int leftStock = product.getProductStock() - cartDTO.getProductQuantity();

            if (leftStock < 0 ){
                throw new ProductException(ResultEnum.PRODUCT_STOCK_ERROR);
            }
            // 将剩余库存设置到product,并更新数据库
            product.setProductStock(leftStock);
            productRepository.save(product);
        }

    }

因为是对List操作,所以加个事务控制 @Transactional

单元测试

@Test
    public void decreaseProductTest() {

        CartDTO cartDTO = new CartDTO();
        cartDTO.setProductId("3");
        cartDTO.setProductQuantity(2);

        productService.decreaseProduct(Arrays.asList(cartDTO));
    }

测试前数据

Spring Cloud实战-03订单微服务与商品微服务之间的调用

Spring Cloud实战-03订单微服务与商品微服务之间的调用

Spring Cloud实战-03订单微服务与商品微服务之间的调用


Controller层

Spring Cloud实战-03订单微服务与商品微服务之间的调用


Order微服务调用接口扣减库存

增加接口方法

ProductClient接口新增方法

Spring Cloud实战-03订单微服务与商品微服务之间的调用

测试下 ,在 FeginClientController 新增个方法 (这个Controller和工程无关哈,仅仅是用来测试用的)

访问 http://localhost:8081/order/decreseProduct

Spring Cloud实战-03订单微服务与商品微服务之间的调用

Spring Cloud实战-03订单微服务与商品微服务之间的调用


整合

Product微服务要提供的功能及Order微服务调用都开发完了,那整合到业务逻辑中吧

@Override
    public OrderDTO createOrder(OrderDTO orderDTO) {

        String orderId = KeyUtil.genUniqueKey();

        //  查询商品信息(调用商品微服务)
        List<String> productIdList = orderDTO.getOrderDetailList().stream()
                .map(OrderDetail::getProductId)
                .collect(Collectors.toList());
        List<Product> productList = productClient.getProductForOrder(productIdList);


        //   计算订单总价

        BigDecimal orderAmout = new BigDecimal(BigInteger.ZERO);
        for (OrderDetail orderDetail: orderDTO.getOrderDetailList()) {
            for (Product product: productList) {
                if (product.getProductId().equals(orderDetail.getProductId())) {
                    //单价*数量
                    orderAmout = product.getProductPrice()
                            .multiply(new BigDecimal(orderDetail.getProductQuantity()))
                            .add(orderAmout);
                    BeanUtils.copyProperties(product, orderDetail);
                    orderDetail.setOrderId(orderId);
                    orderDetail.setDetailId(KeyUtil.genUniqueKey());
                    //订单详情入库
                    orderDetailRepository.save(orderDetail);
                }
            }
        }

        // 扣减库存(调用商品微服务)

        List<CartDTO> cartDTOList = orderDTO.getOrderDetailList().stream()
                .map(e -> new CartDTO(e.getProductId(), e.getProductQuantity()))
                .collect(Collectors.toList());
        productClient.decreseProduct(cartDTOList);

        //订单入库
        Order order = new Order();
        orderDTO.setOrderId(orderId);
        // 复制属性
        BeanUtils.copyProperties(orderDTO, order);
        // 设置其他属性
        order.setOrderAmount(orderAmout);
        order.setOrderStatus(OrderStatusEnum.NEW.getCode());
        order.setPayStatus(PayStatusEnum.WAIT.getCode());
        orderRepository.save(order);

        return orderDTO;
    }

测试

Spring Cloud实战-03订单微服务与商品微服务之间的调用

  [{
      "productId": "1",
       "productQuantity": 2  
},
{
      "productId": "2",
       "productQuantity": 5  
},
{
      "productId": "3",
       "productQuantity": 10  
}]

买 1号商品 2个 ,金额 20.99乘以2 = 41.98
买 2号商品 5个 ,金额 7.5乘以5 = 37.5
买 3号商品 10个 ,金额 15乘以10 = 150
总金额 229.48

原始库存:

Spring Cloud实战-03订单微服务与商品微服务之间的调用

使用POSTMAN测试一把
Spring Cloud实战-03订单微服务与商品微服务之间的调用

检查下总金额,库存扣减,及order_detail中的数据

artisan_order
Spring Cloud实战-03订单微服务与商品微服务之间的调用

order_detail 3条记录
Spring Cloud实战-03订单微服务与商品微服务之间的调用

库存:

Spring Cloud实战-03订单微服务与商品微服务之间的调用

OK


知识点小结

点1

Spring MVC在接收集合请求参数时,需要在Controller方法的集合参数里前添加@RequestBody

List<Product> getProductForOrder(@RequestBody  List<String> productIdList)

点2

当参数中标注了@RequestBody , 则必须使用POST方法
Spring Cloud实战-03订单微服务与商品微服务之间的调用


Github

artisan-product: https://github.com/yangshangwei/springcloud-o2o/tree/master/artisan_order

artisan_order: https://github.com/yangshangwei/springcloud-o2o/tree/master/artisan-product