SpringCloud入門(三)

前言:本課程是在慕課網上學習Spring Cloud微服務實戰 第五章 應用通信 時所做的筆記,供本人複習之用.

目錄

第一章 HTTP與PRC

第二章 RestTemplate的三種使用方法

2.1 第一種使用方式

2.2 第二種使用方式

2.3 第三種使用方式

第三章 負載均衡器Ribbon

3.1 Ribbon概要介紹

3.2 Ribbon源碼追蹤

第四章 Feign

第五章 使用Feign做服務間的通信

5.1 查詢商品詳情

5.1.1 商品服務

5.1.2 訂單服務

5.2 扣庫存

5.2.1 商品服務

5.2.2 訂單服務

5.3 完善整個下單流程

5.3.1 訂單服務


 

第一章 HTTP與PRC

應用間的通信方式主要有兩種HTTP與RPC,SpringCloud與Dubbo正好是這兩種方式的代表.

Dubbo本身的定位就是一個RPC框架,基於Dubbo開發的應用還是要依賴周邊的平臺存在,相比其它RPC框架,Dubbo在服務治理功能上非常完善,不僅提供了服務註冊發現,負載均衡,路由以及面向分佈式集羣的技術能力,還涉及了面向開發測試階段的mocker泛化調用等機制,同時也提供了服務治理和監控的可視化平臺,SpringCloud在沒有出來之前Dubbo在國內應用的相當廣泛,Dubbo的定位始終是一款RPC框架,而SpringCloud的目標是微服務架構下的一站式解決方案,在重啓維護後,Dubbo官方表示,要積極尋求適配到SpringCloud的方式,比如作爲SpringCloud的二進制通信方案來發揮Dubbo的性能優勢,或者Dubbo通過模塊化或者對http的支持適配到SpringCloud,不過當前兩者還是不兼容.

SpringCloud微服務架構下,微服務之間使用的是http restful通信方式,http restful本身輕量易用,適用性強可以很容易的跨語言,跨平臺,或者與已有的系統交互,我們前端舉的例子node.js的Eureka Client也印證了這一點.

SpringCloud通過以下兩種restful調用方式.

RestTemplate與Feign.

第二章 RestTemplate的三種使用方法

RestTemplate是一個http客戶端,功能與java中的httpClient類似,用法上更加簡單,下面我們將用訂單服務調用商品服務的接口.我們可以將訂單服務理解成客戶端,商品服務理解成服務端,

服務端配置application.properties,引入的依賴在上一篇中:

spring.application.name=product
#datasource
spring.datasource.url=jdbc:mysql://localhost:3306/SpringCloud_Sell?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=UTF-8&useSSL=true
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
#Jpa
spring.jpa.open-in-view=true
spring.jpa.properties.hibernate.enable_lazy_load_no_trans=true
spring.jpa.show-sql = true
spring.jpa.hibernate.ddl-auto=update
spring.jpa.database-platform=org.hibernate.dialect.MySQL5InnoDBDialect

eureka.client.service-url.defaultZone = http://localhost:8761/eureka
server.port=8083

 

客戶端配置:

spring.application.name=order
#datasource
spring.datasource.url=jdbc:mysql://localhost:3306/SpringCloud_Sell?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=UTF-8&useSSL=true
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
#Jpa
spring.jpa.open-in-view=true
spring.jpa.properties.hibernate.enable_lazy_load_no_trans=true
spring.jpa.show-sql = true
spring.jpa.hibernate.ddl-auto=update
spring.jpa.database-platform=org.hibernate.dialect.MySQL5InnoDBDialect

eureka.client.service-url.defaultZone = http://localhost:8761/eureka
server.port=8081

服務中心配置:

eureka.client.service-url.defaultZone = http://localhost:8761/eureka/
eureka.client.register-with-eureka=false
eureka.server.enable-self-preservation=false
spring.application.name=eureka
server.port=8761

我們先調用幾個簡單的方法. 

2.1 第一種使用方式

product服務端的controller(我們要在客戶端中進行調用的)

@RestController
public class ServerController {
    @GetMapping("/msg")
    public String msg(){
        return "this is product' msg2";
    }
}

order客戶端中的controller:

直接指定地址和返回值.

    @RequestMapping("/getProductMsg")
    public String getProductMsg(){
        //第一種方式,到那時當有多個地址的時候就有問題了
        RestTemplate restTemplate = new RestTemplate();
        String response = restTemplate.getForObject("http://localhost:8083/msg",String.class);
        log.info("response={}",response);
        return response;
    }

啓動應用,訪問地址 

 訪問成功

這樣的弊端是地址寫死,如果不知道對方的地址,或者對方有兩個地址就會無從下手.

2.2 第二種使用方式

現在我們有兩個服務器

product服務端配置不變.

order客戶端的服務方式變爲:

@RestController
@Slf4j
public class ClientController {
    @Autowired
    private LoadBalancerClient loadBalancerClient;
    @RequestMapping("/getProductMsg")
    public String getProductMsg(){
    ServiceInstance serviceInstance = loadBalancerClient.choose("Product");
    String url = String.format("http://%s:%s",serviceInstance.getHost(),serviceInstance.getPort())+"/msg";
    RestTemplate restTemplate = new RestTemplate();
    String response = restTemplate.getForObject(url,String.class);
    log.info("response={}",response);
    return response;
    }
}

也訪問到了數據

我們這樣通過應用名就訪問到了要調用的服務,但是這種方式還有一些繁瑣.

2.3 第三種使用方式

第三種方式主要是使用了@LoadBalanced註解

新建Config類.

@Component
public class RestTemplateConfig {
    @Bean
    @LoadBalanced
    public RestTemplate restTemplate(){
        return new RestTemplate();
    }
}

product服務端配置不變.

order客戶端的服務方式變爲:

@RestController
@Slf4j
public class ClientController {
    @Autowired
    private RestTemplate restTemplate;
    @RequestMapping("/getProductMsg")
    public String getProductMsg(){
    String response =  restTemplate.getForObject("http://PRODUCT/msg",String.class);
    log.info("response={}",response);
    return response;
    }
}
    

繼續訪問可以獲得信息. 

這三種方式都可以歸結爲使用restTemplate的方式調用應用的接口,值得注意的是這裏有一個負載均衡的問題,它會幫我們去選擇ip地址,至於負載均衡的策略,可以隨機,輪詢等等.

第三章 負載均衡器Ribbon

3.1 Ribbon概要介紹

在講解Eureka的時候,我們也說過它是客戶端發現,Eureka屬於客戶端發現的方式,它的負載均衡是軟負載,也就是客戶端會向服務器拉取已註冊的可用服務信息,然後根據負載均衡策略,直接命中每臺服務器發送請求,整個過程都是在客戶端完成的,並不需要服務器的參與,SpringCloud客戶端的負載均衡就是Ribbon組件,它是基於netflix Ribbon實現的,通過SpringCloud的封裝,可以輕鬆的面向服務的Rest模板請求,自動轉化成客戶端負載均衡服務調用.

RestTemplate Feign Zuul都使用到了Ribbon,SpirngCloud在結合了Ribbon的負載均衡實現中,封裝增加了HttpClient和OkHttp兩種請求端實現,默認使用ribbon對eureka服務發現的負載均衡Client,第二章中介紹了RestTemplate三種實現方式,其中通過添加@LoadBalanced註解或者直接寫代碼的時候使用loadBalanceClient,其實用到的就是Ribbon的組件,添加@LoadBalanced註解後,Ribbon會通過loadBalanceClient自動幫助你基於某種規則比如簡單的輪詢,隨機連接等去連接目標服務,從而很容易使用Ribbnon實現自定義負載均衡算法.

Ribbon實現軟負載均衡核心有三點

1.服務發現,也就是發現依賴服務的列表.也就是依據服務的名字,找出服務中的所有實例.

2.服務選擇規則,依據規則策略,如何從多個服務中選擇一個有效的服務.

3.服務監聽,也就是檢測失效的服務,做到高效剔除.

Ribbon的主要組件

ServerList,IRule,ServerListFilter等

主要流程是這樣的,通過ServerList獲得所有的可用服務列表,然後通過ServerListFilter過濾掉一部分地址,最後剩下的地址中通過IRule選擇一個實例作爲最終目標結果.

3.2 Ribbon源碼追蹤

我們用2.2的方式來查看源碼.

ServiceInstance serviceInstance = loadBalancerClient.choose("Product");

idea:鼠標放在choose上,在按alt+ctrl+b找到其實現.

找到choose源碼,getServer就是要將服務列表找出來

public class RibbonLoadBalancerClient implements LoadBalancerClient {

//...省略

@Override
	public ServiceInstance choose(String serviceId) {
		return choose(serviceId, null);
	}

public ServiceInstance choose(String serviceId, Object hint) {
		Server server = getServer(getLoadBalancer(serviceId), hint);
		if (server == null) {
			return null;
		}
		return new RibbonServer(serviceId, server, isSecure(server, serviceId),
				serverIntrospector(serviceId).getMetadata(server));
	}
//...省略

}

我們點進getServer中,看到其用ILoadBalancer在找.

protected Server getServer(ILoadBalancer loadBalancer, Object hint) {
		if (loadBalancer == null) {
			return null;
		}
		// Use 'default' on a null hint, or just pass it on?
		return loadBalancer.chooseServer(hint != null ? hint : "default");
	}

我們點進chooseServer中,選擇BaseLoadBalancer類,如圖1所示,其中的getAllServers就是獲取所有的服務列表.

@Override
    public List<Server> getAllServers() {
        return Collections.unmodifiableList(allServerList);
    }
圖1

再在BaseLoadBalancer類中找到chooseServer方法,rule.choose表示用規則選擇服務,我們點進rule

public Server chooseServer(Object key) {
        if (counter == null) {
            counter = createCounter();
        }
        counter.increment();
        if (rule == null) {
            return null;
        } else {
            try {
                return rule.choose(key);
            } catch (Exception e) {
                logger.warn("LoadBalancer [{}]:  Error choosing server for key {}", name, key, e);
                return null;
            }
        }
    }

 我們發現rule使用的是默認的規則RoundRobinRule,這是一個輪詢的方式.

private final static IRule DEFAULT_RULE = new RoundRobinRule();
private final static SerialPingStrategy DEFAULT_PING_STRATEGY = new SerialPingStrategy();
private static final String DEFAULT_NAME = "default";
private static final String PREFIX = "LoadBalancer_";
protected IRule rule = DEFAULT_RULE;

我們在DynamicServerListLoadBalancer打上斷點也能發現其用的是輪詢的規則.

通過如下可以切換規,比如圖2就表示換成隨機規則.在springcloud的官方文檔中搜索Ribbon關鍵字找到Customizing default for all Ribbon Clients可以查看更多相關信息.

第四章 Feign

Feign是一個聲明式REST客戶端,採用了接口加註解的方式,Feign內部也使用了Ribbon.

前面使用了RestTemplate,這章使用Feign進行應用間的通信.訂單服務調用商品服務.

在訂單服務中增加Feign的依賴.

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

在啓動主類上加註解@EnableFeignClients

@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class OrderApplication {
	public static void main(String[] args) {
		SpringApplication.run(OrderApplication.class, args);
	}
}

在訂單服務中新建接口,name說明的是要應用哪個應用,getmapping指明要用應用中的哪個controller.方法名只是一個標識.

@FeignClient(name="product")
public interface ProductClient {
    @GetMapping("msg")
    String productMsg();
}

在訂單中直接注入,然後當作方法調用即可.

@RestController
@Slf4j
public class ClientController {
    @Autowired
    private ProductClient productClient;

    @GetMapping("/getProductMsg")
    public String getProductMsg(){
        String response = productClient.productMsg();
        log.info("response={}",response);
        return response;
    }

}

 

第五章 使用Feign做服務間的通信

5.1 查詢商品詳情

我們要用訂單服務去調用商品服務,從訂單服務中給出商品的id,然後商品服務根據id查詢商品信息,並將查詢到的商品信息返回給訂單服務.

寫之前有兩點需要注意:@ResquestBody註解必須要用PostMapping的形式來映射,無參,單個參數@RequestParam,或者@PathVariable註解都可以用get.

5.1.1 商品服務

主要是傳入productId,返回product詳細信息集合.

controller:

@RestController
@RequestMapping("/product")
public class ProductController {
    @Autowired
    private ProductService productService;
/*獲取商品列表(給訂單服務用的)*/
    @PostMapping("/listForOrder")
    public List<ProductInfo> listForOrder(@RequestBody List<String> productList){
        return productService.findByProductIdIn(productList);
    }
}

service:

@Override
    public List<ProductInfo> findByProductIdIn(List<String> productIdList) {
        return productInfoRepository.findByProductIdIn(productIdList);
    }

dao:

public interface ProductInfoRepository extends JpaRepository<ProductInfo,String> {

    List<ProductInfo> findByProductIdIn(List<String> productId);
}

5.1.2 訂單服務

指明要使用的服務

@FeignClient(name="product")
public interface ProductClient {
    @PostMapping("/product/listForOrder")
    List<ProductInfo> listForOrder(@RequestBody  List<String> productList);
}

 在controller中進行調用

@GetMapping("/getProductList")
    public String getProductList(){
        List<ProductInfo> productInfoList = productClient.listForOrder(Arrays.asList("164103465734242707"));
        log.info("response:{}",productInfoList);
        return "ok";
    }

 訪問地址http://localhost:8081/getProductList,獲得信息成功

5.2 扣庫存

訂單服務將要扣庫存的商品id與數量傳入商品服務,由商品服務進行商品數量的更改.

5.2.1 商品服務

數據對象:

@Data
public class CartDTO {
    private String productId;
    private Integer productQuantity;


    public CartDTO(String productId, Integer productQuantity) {
        this.productId = productId;
        this.productQuantity = productQuantity;
    }
}

service層:

    @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);
        }
    }

controller層:

@RestController
@RequestMapping("/product")
public class ProductController {
    @Autowired
    private ProductService productService;
    @PostMapping("/decreaseStock")
    public void decreaseStock(@RequestBody List<CartDTO> cartDTOList){
        productService.decreaseStock(cartDTOList);
    }
}

5.2.2 訂單服務

指明商品服務配置:

@FeignClient(name="product")
public interface ProductClient {

    @PostMapping("/product/decreaseStock")
    void decreaseStock(@RequestBody List<CartDTO> cartDTOList);
}

controller進行調用:

@GetMapping("productDecreaseStock")
    public String productDecreaseStock(){
        productClient.decreaseStock(Arrays.asList(new CartDTO("164103465734242707",3)));
        return "ok";
    }

運行,商品庫存減少.

5.3 完善整個下單流程

前臺的請求參數就是用戶信息以及購買的各種商品數量,類似下圖所示

下單流程: 查詢商品信息->計算總價->扣庫存->訂單入庫

5.3.1 訂單服務

controller層:

@Autowired
    private OrderService orderService;
    @PostMapping("/create")
    public ResultVO<Map<String,String>> create(@Valid OrderForm orderForm, BindingResult bindingResult){
        if(bindingResult.hasErrors()){
            log.error("創建訂單參數不正確 ={}",orderForm);
            throw new OrderException(1,bindingResult.getFieldError().getDefaultMessage());
        }
        OrderDTO orderDTO = OrderForm2OrderDTO.convert(orderForm);
        //判斷購物車是否爲空
        if(CollectionUtils.isEmpty(orderDTO.getOrderDetailList())){
            log.error("創建訂單信息失敗,購物車爲空");
            throw new OrderException(-1,"購物車爲空");
        }
        OrderDTO result = orderService.create(orderDTO);
        Map<String,String> map = new HashMap<>();
        map.put("orderId",result.getOrderId());
        return ResultVOUtil.success(map);
    }

service層:

 

@Override
    public OrderDTO create(OrderDTO orderDTO) {
        String orderId = KeyUtil.getUniqueKey();
        //查詢商品信息(調用商品服務)
        List<String> productIdList = orderDTO.getOrderDetailList().stream()
                .map(OrderDetail::getProductId)
                .collect(Collectors.toList());
        log.info("查詢商品信息(調用商品服務)");
        List<ProductInfo> productInfoList = productClient.listForOrder(productIdList);
        //計算總價
        BigDecimal orderAmout = new BigDecimal(BigInteger.ZERO);
        for (OrderDetail orderDetail: orderDTO.getOrderDetailList()) {
            for (ProductInfo 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.getUniqueKey());
                    //訂單詳情入庫
                    orderDetailRepository.save(orderDetail);
                }
            }
        }

        //扣庫存(調用商品服務)
        List<CartDTO> decreaseStockInputList = orderDTO.getOrderDetailList().stream()
                .map(e -> new CartDTO(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;
    }

到這裏基本業務流程基本走通了,下一篇要進行多模塊的構建.

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章