源码分析:通过Spring Boot构建一个购物车微服务 | 云原生应用开发系列6
Posted 大魏分享
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了源码分析:通过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项测试成功:
魏新宇
"大魏分享"运营者、红帽资深解决方案架构师
专注开源云计算、容器及自动化运维在金融行业的推广
拥有MBA、ITIL V3、Cobit5、C-STAR、TOGAF9.1(鉴定级)等管理认证。
拥有红帽RHCE/RHCA、VMware VCP-DCV、VCP-DT、VCP-Network、VCP-Cloud、AIX、HPUX等技术认证
以上是关于源码分析:通过Spring Boot构建一个购物车微服务 | 云原生应用开发系列6的主要内容,如果未能解决你的问题,请参考以下文章
毕业设计 Spring Boot的网上购物商城系统(含源码+论文)
头秃系列,二十三张图带你从源码分析Spring Boot 启动流程~
Spring Boot源码分析@EnableAutoConfiguration注解@AutoConfigurationImportSelector注解的处理