源码分析:通过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项测试成功:

图片





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