源碼分析:通過Spring Boot構建一個購物車微服務 | 雲原生應用開發系列6


一、Spring Boot和Spring Cloud的一些特性

Spring Boot非常適合Web應用程序開發

spring-boot-starter-web starter提供了所需的依賴項

  • 包括嵌入式HTTP服務器(Tomcat)

  • 可以使用Spring MVC或JAX-RS開發REST API

  • Spring MVC:通用模型 - 視圖 - 控制器Web框架

  • JAX-RS:標準Java EE REST API規範



在Spring MVC中,您可以創建REST應用程序。

您創建一個使用@RestController註釋的控制器類。

圖片

然後定義使用@RequestMapping註釋的處理程序方法。

您還可以使用@PathVariable註釋的URI模板參數。 此處顯示的代碼示例公開了REST端點/ users / {user}。 {user}的代碼是用於自定義REST請求的路徑變量。 Spring Boot提供了一個Jackson ObjectMapper的自動配置,用於JSON有效負載和POJO對象之間的自動編組。


Spring在構建REST資源時,通常使用@RestController和@RequestMapping來定義。而JavaEE定義的時候,通常使用JAX-RS實現@Path。當然,在Spring中也可以使用JAX-RS。


作爲開發的另一種選擇,您可以使用JAX-RS。 默認的Spring Boot JAX-RS實現是Jersey,它是Glassfish Jax-RS實現。 您可以使用spring-boot-starter-jersey Spring Boot啓動器。 Spring Boot提供Jersey servlet和Jackson數據綁定的自動配置。 您可以使用標準的JAX-RS註釋構建REST端點,例如@ javax.ws.rs.Path,@ javax.ws.rs.Produces和javax.ws.rs.GET。此代碼示例演示如何使用JAX-RS在Spring中構建REST資源。



REST資源類需要在Jersey上下文中註冊。這裏顯示了一個例子。

Spring Boot包括對嵌入式Tomcat,Jetty和Undertow服務器的支持。 默認情況下,Spring Boot啓動程序(特別是spring-boot-starter-web)使用Tomcat作爲嵌入式容器。要使用備用服務器,請排除Tomcat依賴項幷包含所需的依賴項。 這裏的代碼片段顯示瞭如何排除Tomcat幷包含Undertow服務器所需的依賴關係。



  • Spring Framework爲使用SQL數據庫提供了廣泛的支持。 您可以使用JdbcTemplate直接進行JDBC訪問。 您還可以將JPA對象關係映射與Hibernate一起使用。 Spring Data爲支持SQL,NoSQL,MapReduce框架和基於雲的數據服務的數據訪問提供了基於Spring的編程模型。 Spring Boot提供以下啓動器:spring-boot-starter-jdbc和spring-boot-starter-data-jpa。 Spring Boot的一個很好的功能是內存數據庫的自動配置,非常適合測試。 此外,Spring Boot還提供具有外部配置屬性的數據源的自動配置。

Spring Framework還提供實體類掃描,以及Spring Data Repository類的自動註冊。 Spring JPA存儲庫是封裝數據訪問的接口。 JPA查詢是從方法名稱自動創建的。這裏顯示了一個例子。

Spring Boot配置可以外部化,以便相同的應用程序代碼可以在不同的環境中工作。 您可以使用屬性文件,YAML文件,環境變量和命令行參數來外部化配置。 可以使用@Value註釋將屬性值直接注入bean,通過Spring的Environment抽象訪問,或通過@ConfigurationProperties綁定到結構化對象。



Spring Boot使用一個有序的序列來指定屬性,以便允許合理地覆蓋值。順序如下:

@TestPropertySource

命令行參數

Java系統屬性

OS環境變量

打包JAR之外的特定於配置文件的應用程序屬性

打包的JAR中的特定於配置文件的應用程序屬性

默認屬性



使用@Value(“$ {property}”)註釋來注入配置屬性可能很麻煩。 Spring Boot提供了另一種選擇。您可以定義強類型bean來管理和驗證應用程序的配置。

下面的代碼示例定義了以下屬性:


foo.enabled,默認爲false

foo.remote-address,具有可以從String強制轉換的類型

foo.security.username,具有嵌套安全性

foo.security.roles,帶有String集合

您可以像使用任何其他bean一樣注入此配置。

Spring Profiles提供了一種隔離應用程序配置部分的方法,並使每個部分僅在某些環境中可用 - 例如,dev,QA或production。


任何@Component或@Configuration都可以用@Profile標記,以限制何時加載。


特定於配置文件的屬性文件名爲application- {profile} .properties。


當配置文件處於活動狀態時,您可以覆蓋application.properties中的默認屬性。




  • 您可以通過以下任何方式指定活動配置文件:使用-Dspring.profiles.active = dev作爲命令行上的系統屬性 作爲使用導出SPRING_PROFILES_ACTIVE = dev的環境變量 在使用spring.profiles.active = dev的應用程序屬性中 在使用@ActiveProfiles(“test”)的測試用例中

圖片



Spring Cloud是一個用於開發雲原生應用程序的框架。Spring Cloud提供了通用設計模式的實現,以支持雲本機應用程序的開發。 Spring Cloud提供的解決方案:


  • 集中配置管理

  • 服務註冊和發現

  • 負載均衡

  • 斷路器

  • 異步通信

  • 分佈式跟蹤






Spring Cloud還提供第三方工具和庫的集成和抽象:


  • Netflix OSS Eureka服務註冊表

  • HashiCorp服務登記

  • Netflix OSS Hystrix斷路器和隔板

  • Netflix OSS功能區客戶端負載均衡器

  • Apache Kafka和RabbitMQ消息代理

  • Zipkin分佈式追蹤



Spring Cloud Kubernetes提供Spring Cloud與Kubernetes和OpenShift的集成。 它由Red Hat Fabric8.io團隊發起,現在由Spring Cloud Incubator託管。

功能包括以下內容:

  • 帶有ConfigMaps和secret的Spring Boot配置

  • 當在ConfigMap中檢測到更改時,PropertySource重新加載以觸發應用程序重新加載

  • pod運行狀況指示器,用於將特定於pod的運行狀況數據添加到Spring Actuator運行狀況端點

  • 在Kubernetes上運行時,Kubernetes配置文件自動配置

  • Kubernetes的功能區發現

  • Archaius-a Netflix OSS配置管理庫-ConfigMap屬性源

  • 透明度 - 當應用程序在Kubernetes / OpenShift之外運行時,Spring Cloud Kubernetes不會中斷



在Red Hat Fuse Integration Services 2.x版中,Spring Boot是在OpenShift上開發Camel應用程序的首選框架。 啓動器模塊是camel-spring-boot-starter。 CamelContext的自動配置在Spring應用程序上下文中註冊。 使用@Component註釋的Camel路由會自動檢測並注入CamelContext。




二:實驗展現:構建購物車


在本實驗中,您添加了爲Coolstore應用程序的購物車微服務公開REST API的功能。實驗室從上一個實驗室的解決方案代碼開始,包括您使用的其他文件。



爲購物車微服務實現和公開REST API

查看並運行REST API的單元測試


應用架構

購物車微服務由一個Maven項目組成,該項目內部由許多服務對象組成:

PriceCalculationService包含用於計算購物車的運費和總價值的邏輯。

CatalogService負責調用目錄服務以獲取產品數據。

ShoppingCartService負責管理購物車。

CartEndpoint包含用於訪問購物車微服務的REST API。


在本實驗中,您將添加REST API的實現以訪問CartEndpoint類中的購物車微服務。



啓動Red Hat Developer Studio。 選擇文件→導入。 在“導入”對話框中,選擇“Maven”→“現有Maven項目”,然後單擊“下一步”。 單擊“瀏覽”並導航到〜/ appmod_springboot_experienced / lab-02。 這是您解壓縮本實驗的代碼的目錄。 確保爲項目選中了/pom.xml框,然後單擊Finish。 導入後,驗證您是否看到該項目。

查看購物車服務項目的pom.xml文件,並注意以下事項: 在dependencyManagement部分中,導入spring-boot-dependencies物料清單(BOM)POM。此POM包含特定Spring Boot版本支持的所有依賴項的策劃列表。實際上,這意味着您不必手動跟蹤在構建配置中添加的依賴項的版本,因爲Spring Boot正在爲您管理。升級Spring Boot本身時,依賴關係也會以一致的方式升級。 在Red Hat OpenShift Application Runtimes環境中認證的Spring Boot版本是1.5.10.RELEASE。 spring-boot-maven插件用於構建可執行的JAR文件(fat JAR)。插件的重新打包目標創建了一個可自動執行的JAR(或WAR)文件。




使用Spring Boot開發一個購物車微服務使用Spring Boot,可以使用不同的技術來構建REST API。您可以使用Spring MVC框架或實現JAX-RS規範的框架。 對於購物車服務,需要以下REST端點: GET / cart / {cartId}按ID獲取購物車。 POST / cart / {cartId} / {itemId} / {quantity}將商品添加到購物車。 DELETE / cart / {cartId} / {itemId} / {quantity}從購物車中刪除商品。 POST / cart / checkout / {cartId}檢查購物車。


我們查看源碼,進行分析:


package com.redhat.coolstore.cart.rest;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.RestController;

import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

import org.springframework.beans.factory.annotation.Autowired;

import com.redhat.coolstore.cart.service.ShoppingCartService;

import org.springframework.web.bind.annotation.GetMapping;

import org.springframework.web.bind.annotation.PathVariable;

import com.redhat.coolstore.cart.model.ShoppingCart;

import org.springframework.web.bind.annotation.PostMapping;

import org.springframework.web.bind.annotation.DeleteMapping;

@RestController

@RequestMapping("/cart")

//@RequestMapping批註標識資源類或類方法爲其請求的URI路徑。購物車服務的URI路徑是/ cart。@RestController註釋將此類標識爲REST資源//

public class CartEndpoint {

private static final Logger LOG = LoggerFactory.getLogger(CartEndpoint.class);

//使用SLF4J API配置一個logger//

    @Autowired

    private ShoppingCartService shoppingCartService;

//使用Spring @Autowired註釋注入ShoppingCartService//

    @GetMapping("/{cartId}")

    public ShoppingCart getCart(@PathVariable String cartId) {


        return shoppingCartService.getShoppingCart(cartId);

        

        

    }

//@GetMapping表示帶註釋的方法響應HTTP GET請求。URI部分附加到類級別定義的基本路徑。 {cartId}表示模板參數名稱。@PathVariable將URI模板參數的值綁定到方法變量。此代碼將調用委託給ShoppingCartService.getShoppingCart方法。//

    @PostMapping("/{cartId}/{itemId}/{quantity}")

    public ShoppingCart add(@PathVariable String cartId,

                            @PathVariable String itemId,

                            @PathVariable int quantity) {


            return shoppingCartService.addToCart(cartId, itemId, quantity);

    }

//此方法實現REST POST / cart / {cartId} / {itemId} / {quantity}端點。//

    @DeleteMapping("/{cartId}/{itemId}/{quantity}")

    public ShoppingCart delete(@PathVariable String cartId,

                                    @PathVariable String itemId,

                                    @PathVariable int quantity) {


        return shoppingCartService.removeFromCart(cartId, itemId, quantity);

    }

//此方法實現DELETE / cart / {cartId} / {itemId} / {quantity}端點。//

    @PostMapping("/checkout/{cartId}")

    public ShoppingCart checkout(@PathVariable String cartId) {


        ShoppingCart cart = shoppingCartService.checkoutShoppingCart(cartId);

        LOG.info("ShoppingCart " + cart + " checked out");


        return cart;

    }

}

//此方法實現REST POST / cart / checkout / {cartId}端點。目前,只需記錄購物車已簽出的事實就足夠了。//





查看並運行端到端集成測試此時,您已準備好所有部分。在本節中,您將查看並運行購物車服務的端到端測試(或集成測試)。 Spring Boot允許集成測試,而無需實際部署應用程序或連接到其他基礎架構。 Spring Boot應用程序作爲測試本身的一部分進行自舉。所需要的只是依賴於spring-boot-starter-test啓動器。 您可以使用不同的技術在集成測試中測試REST端點。 REST Assured是一種流暢而優雅的Java DSL,用於簡化基於REST的服務的測試,可用於驗證和驗證這些服務的響應。 Rest Assured使得驗證JSON或XML有效負載變得特別容易。 要模擬遠程目錄服務,您可以使用WireMock框架,就像在CatalogService服務的測試中一樣。 Spring Boot應用程序 - 更具體地說,是CatalogService實現 - 期望將catalog.service.url系統屬性設置爲遠程目錄服務的URL。將此屬性注入測試的一種方法是利用Spring配置文件和Spring Boot對特定於配置文件的屬性的支持。 在項目的src / test / resources文件夾中,查看名爲application-test.properties的文件:

無需爲此屬性設置值,因爲實際的URL(特別是WireMock服務器綁定的端口)在執行測試本身之前是未知的。您將實際URL注入測試代碼。 Spring Boot從類路徑根目錄中的application.properties和application- {profile} .properties文件加載屬性,並將它們添加到Spring環境中。在這種情況下,僅在測試配置文件處於活動狀態時才加載application-t\

est.properties。




選擇項目的src / test / java文件夾。 查看com.redhat.coolstore.cart.rest包中的CartEndpointTest類。

此批註激活測試配置文件,因此application-test.properties文件將加載到Spring上下文中。


我們查看測試的源碼:

圖片


package com.redhat.coolstore.cart.rest;


import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;

import static com.github.tomakehurst.wiremock.client.WireMock.get;

import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;

import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;

import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig;


import static io.restassured.RestAssured.given;

import static org.hamcrest.Matchers.equalTo;

import static org.hamcrest.Matchers.hasItems;

import static org.hamcrest.Matchers.hasSize;


import java.io.InputStream;

import java.nio.charset.Charset;


import org.apache.commons.io.IOUtils;

import org.junit.Before;

import org.junit.Rule;

import org.junit.Test;

import org.junit.runner.RunWith;

//SpringRunner類提供了JUnit測試框架和Spring框架之間的集成。當使用SpringRunner,Spring應用程序上下文時 - 在Spring Boot的情況下,這是Spring Boot應用程序本身 - 在測試和啓用Spring組件的依賴注入之前的bootstraps。//

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.boot.context.embedded.LocalServerPort;

import org.springframework.boot.test.context.SpringBootTest;

import org.springframework.test.annotation.DirtiesContext;

import org.springframework.test.context.ActiveProfiles;

import org.springframework.test.context.junit4.SpringRunner;

import org.springframework.test.util.ReflectionTestUtils;


import com.github.tomakehurst.wiremock.junit.WireMockRule;

import com.redhat.coolstore.cart.service.CatalogService;


import io.restassured.RestAssured;

import io.restassured.http.ContentType;


@ActiveProfiles("test")

@RunWith(SpringRunner.class)

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)

//@SpringBootTest註釋在常規Spring測試框架上提供了一些特定於Spring Boot的增強功能。特別是,SpringBootTest.WebEnvironment.RANDOM_PORT環境加載了一個嵌入式WebApplicationContext並提供了一個真正的servlet環境。

嵌入式servlet容器(在本例中爲Tomcat)在隨機端口上啓動和監聽。

//

public class CartEndpointTest {


    @LocalServerPort

    private int port;


    @Rule

    public WireMockRule wireMockRule = new WireMockRule(wireMockConfig().dynamicPort());


    @Autowired

    private CatalogService catalogService;


    @Before

    public void beforeTest() throws Exception {

        RestAssured.baseURI = String.format("http://localhost:%d/cart", port);

        ReflectionTestUtils.setField(catalogService, null, "catalogServiceUrl", "http://localhost:" + wireMockRule.port(), null);

        initWireMockServer();

    }


    @Test

    public void retrieveCartById() throws Exception {

        given().get("/{cartId}", "123456")

            .then()

            .assertThat()

            .statusCode(200)

            .contentType(ContentType.JSON)

            .body("id", equalTo("123456"))

            .body("cartItemTotal", equalTo(0.0f))

            .body("shoppingCartItemList", hasSize(0));

    }


    @Test

    @DirtiesContext

//爲了能夠測試調用遠程目錄服務的REST端點,可以使用WireMock模擬目錄服務。由於WireMock服務器綁定到隨機端口(對於每個測試方法可能都是不同的端口),因此必須使用ReflectionTestUtils將實際的WireMock URL注入CatalogService實例。


使用WireMock時,會爲每個測試方法實例化WireMock服務器的新實例,並綁定到不同的端口。因此,還必須爲使用WireMock服務器的測試方法重新創建Spring上下文。這可以通過使用Spring @DirtiesContext註釋來註釋這些測試方法來完成。

//

    public void addItemToCart() throws Exception {


        given().post("/{cartId}/{itemId}/{quantity}", "234567", "111111", new Integer(1))

            .then()

            .assertThat()

            .statusCode(200)

            .contentType(ContentType.JSON)

            .body("id", equalTo("234567"))

            .body("cartItemTotal", equalTo(new Float(100.0)))

            .body("shoppingCartItemList", hasSize(1))

            .body("shoppingCartItemList.product.itemId", hasItems("111111"))

            .body("shoppingCartItemList.price", hasItems(new Float(100.0)))

            .body("shoppingCartItemList.quantity", hasItems(new Integer(1)));

    }


    @Test

    @DirtiesContext

    public void addExistingItemToCart() throws Exception {


        given().post("/{cartId}/{itemId}/{quantity}", "345678", "111111", new Integer(1));

        given().post("/{cartId}/{itemId}/{quantity}", "345678", "111111", new Integer(1))

            .then()

            .assertThat()

            .statusCode(200)

            .contentType(ContentType.JSON)

            .body("id", equalTo("345678"))

            .body("cartItemTotal", equalTo(new Float(200.0)))

            .body("shoppingCartItemList", hasSize(1))

            .body("shoppingCartItemList.product.itemId", hasItems("111111"))

            .body("shoppingCartItemList.price", hasItems(new Float(100.0)))

            .body("shoppingCartItemList.quantity", hasItems(new Integer(2)));

    }


    @Test

    @DirtiesContext

    public void addItemToCartWhenCatalogServiceThrowsError() throws Exception {


        given().post("/{cartId}/{itemId}/{quantity}", "234567", "error", new Integer(1))

            .then()

            .assertThat()

            .statusCode(500);

    }


    @Test

    @DirtiesContext

    public void removeAllInstancesOfItemFromCart() throws Exception {


        given().post("/{cartId}/{itemId}/{quantity}", "456789", "111111", new Integer(2));

        given().delete("/{cartId}/{itemId}/{quantity}", "456789", "111111", new Integer(2))

            .then()

            .assertThat()

            .statusCode(200)

            .contentType(ContentType.JSON)

            .body("id", equalTo("456789"))

            .body("cartItemTotal", equalTo(new Float(0.0)))

            .body("shoppingCartItemList", hasSize(0));

    }


    @Test

    @DirtiesContext

    public void removeSomeInstancesOfItemFromCart() throws Exception {


        given().post("/{cartId}/{itemId}/{quantity}", "567890", "111111", new Integer(3));

        given().delete("/{cartId}/{itemId}/{quantity}", "567890", "111111", new Integer(1))

            .then()

            .assertThat()

            .statusCode(200)

            .contentType(ContentType.JSON)

            .body("id", equalTo("567890"))

            .body("cartItemTotal", equalTo(new Float(200.0)))

            .body("shoppingCartItemList", hasSize(1))

            .body("shoppingCartItemList.quantity", hasItems(new Integer(2)));

    }


    @Test

    @DirtiesContext

    public void checkoutCart() throws Exception {


        given().post("/{cartId}/{itemId}/{quantity}", "678901", "111111", new Integer(3));

        given().post("/checkout/{cartId}", "678901")

            .then()

            .assertThat()

            .statusCode(200)

            .contentType(ContentType.JSON)

            .body("id", equalTo("678901"))

            .body("cartItemTotal", equalTo(new Float(0.0)))

            .body("shoppingCartItemList", hasSize(0));

    }


    private void initWireMockServer() throws Exception {

        InputStream isresp = Thread.currentThread().getContextClassLoader().getResourceAsStream("catalog-response.json");


        stubFor(get(urlEqualTo("/product/111111")).willReturn(

                aResponse().withStatus(200).withHeader("Content-type", "application/json").withBody(IOUtils.toString(isresp, Charset.defaultCharset()))));


        stubFor(get(urlEqualTo("/product/error")).willReturn(

                aResponse().withStatus(500)));

    }


}



使用Red Hat Developer Studio中的JUnit測試運行器運行測試。


或者,在命令行中使用Maven:


開始測試:



測試過程中的打印:

圖片


14項測試成功:

圖片





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