如何将AI能力与大数据技术结合,助力数据分析治理等工作的效率大幅提升,优化大数据引擎的性能及成本? 了解详情
写点什么

使用 Spring Boot 创建微服务

  • 2015-05-18
  • 本文字数:21821 字

    阅读完需:约 72 分钟

过去几年以来,“微服务架构”的概念已经在软件开发领域获得了一个稳定的基础。作为“面向服务架构”(SOA)的一个继任者,微服务同样也可以被归类为“分布式系统”这一类,并且进一步发扬了 SOA 中的许多概念与实践。不过,它们在不同之处在于每个单一服务所应承担的责任范围。在 SOA 中,每个服务将负责处理广范围的功能与数据领域,而微服务的一种通用指南则认为,它所负责的部分是管理一个单独的数据领域,以及围绕着该领域的相关功能。使用分布式系统方式的目的是将整体性的服务基础设施解耦为个别的可扩展子系统,可以通过垂直分片的方式将这些子系统组织在一起,并通过一种通用的传输方式将它们进行相关连接。

在整体性的基础设施中,构成系统的服务在逻辑上是在相同的代码基础与部署单元中组织的。这就能够通过相同的运行时对多个服务之间的相互依赖进行管理,同时也意味着在系统的多个组件中能够共享通用的模型与资源。在整体性基础设施中的子系统之间的相互连接性意味着,通过抽象与功能性函数,可以实现对业务逻辑与数据领域的极大的重用性。虽然这种重用通常是通过紧耦合的方式实现的,但它也存在着一个潜在的好处,就是易于确定某个单一的变更将会对整个系统带来怎样的影响。但为了实现这种便利性,要付出的代价就是牺牲了整个基础设施中单个组织的可伸缩性,同时也意味着整个系统的能力受限于其可伸缩性最薄弱的环节。

在分布式系统中,整体性系统的组件被解耦为个别的部署单元,这些部署单元能够独立地根据可伸缩性的需求自行升级,而不必理会其它子系统的情况。这也意味着整个系统的资源能够被更加有效地利用,并且由于组件之间的相互依赖性不再由运行时环境进行管理,因此它们之间可以通过相对灵活的契约进行相互交互。在传统的 SOA 架构中,服务的边界之内可以封装有关某个业务逻辑的大量功能,并且可以潜在地将大量数据领域集中在一起。而微服务架构不仅继承了系统分布式的概念,同时也承诺只对一个单一的业务功能和数据领域进行管理,这意味着从逻辑上控制某个子系统将变得非常容易。同时也意味着管理子系统的文档化与测试的范围也将变得更简单,因此在这两方面的涵盖程度理应有所提高。

与 SOA 架构一样,微服务架构也必须通过某种通用的传输方式进行相互连接,而这些年以来,HTTP 已经被证明是完成这一任务的一样强大的手段。除此之外还存在着多种选择,例如二进制传输协议以及消息代理,微服务架构中并没有明显地倾向于其中任何一种方式,主要的选择依据是看那些能够在服务之间建立互通信的类库的成熟度与可用性。作为一种成熟的传输协议,几乎每种编程语言与框架都提供了 HTTP 的客户端类库,因此它作为服务间互通信的协议是一个优秀的选择。微服务架构对于与服务交互的无状态性这一方面有着特别的要求,无论采用了哪种底层协议,微服务都应该保持通信的无状态性,并且遵循 RESTful 范式以求实现这一点,这在业界基本已经达成了很好的共识。这就意味着对于某个微服务的每个请求与响应必须保证所调用的方法中的状态必须始终保持可用。说得更明白一点,就是指该服务不能够根据之前的交互行为对于每个请求中所需的数据进行任何假设。保证了正确的 REST 实现,也就意味着微服务本质上就是为了大规模而设计的,这也确保了对于任何一个服务的后续部署能够将停机时间减至最低、甚至做到无停机时间。

要充分了解如何切分一个整体性的架构,并创建微服务可能会存在一些困难,尤其在遗留的代码中,服务边界之间的数据领域通常是紧耦合的。根据经验来看,可以根据某个特定业务功能的边界对基础设施进行垂直切分。多个微服务能够在某个垂直分片的上下文中以协作方式一起运行。举例来说,设想某个电子商务网站的功能,从登陆页面开始,到客户与某个产品进行交互的页面,再到客户购买某个产品的页面,这一连串的业务功能之间存在着清晰的界线。可以将这一套流程分解为多个垂直分片,包括查看产品详细信息、将某个产品加入“购物车”、以及对一个或多个产品下订单。在客户查看产品信息的这个业务上下文中,可能会存在多个微服务,用于处理获取某个特定产品并将其详细信息展现给用户的流程。再举一个例子,在网站的登陆页面中,可能会显示大量产品的名称、图片以及价格。该页面可以从两个后台微服务中获取这些细节信息:一个微服务用于提供产品信息,另一个用于获取每个产品的价格。当用户选中某个特定的产品后,网站可以调用另外两个微服务,它们将用于为用户提供产品的评分与客户的评价。因此,为了提供用于“查看产品详细信息”业务功能在架构上的垂直分片,这个分片或许要通过四种后台微服务的结合才得以实现。

在“产品”这个垂直分片上的每个微服务都是对于“产品”这个领域中不同部分的实现,而每个微服务都具备根据系统的需求进行自我伸缩,并为系统所用的能力。可以想象,负责提供登陆页面用户体验的服务需要应对的请求数量,要远远大于那些提供某个产品详细信息的服务所应对的请求。它们甚至可能是基于不同的技术决策所设计的,例如缓存策略,而在展示产品评分与客户评论的服务中就不会用到这种技术。因为微服务能够根据功能选择适当的技术决策,因此能够更高效地利用资源。而在整体性的架构中,产品评分与客户评价服务则不得不屈从于产品信息与价格服务对于可伸缩性与可用性的需求。

不过,微服务的复杂度与代码的大小没有任何联系。有一种常见的误解认为,微服务的代码量也应该遵循“微”这个概念,但这种说法并不成立,只要你考虑一下微服务构架所试图实现的目标就知道。这个目标是将服务分解为一种分布式系统,而每个服务的复杂度所需的代码量完全于它本身。“微”这个术语表示了这种将职责分散在不同的子系统中的模式,而不是指代码量。不过,由于一个微服务的职责只限制在系统的某个垂直分片中的某个单一功能,因此它的代码通常比较简洁、易于理解、并且能够通过较小的部署单元进行发布。对于微服务有一种推荐的模式,就是将这些服务与运行它们所需的资源一起发布。这也意味着微服务的可部署单元通常包含了它们自己的运行时,并且能够单独运行,这大大减少了与部署相关的运维工作。

过去,部署 Java web 应用程序的方式往往包括一些笨重的、经过预先配置的应用服务器,这些服务器将把档案文件进行解压缩,部署在一个规定的、并且通常是有状态的运行时环境中。为了解压缩某个档案文件,并且开始运行新的应用程序代码,这些应用服务器有可能会产生几十分钟的停机时间,这就造成对更新的迭代变得十分困难,并且从运维的角度来看,也很难接受对某个系统进行多个部署的流程。随着各种各样的框架开始不断进化,以支持微服务的开发,对于代码进行打包以实现部署的流程也在不断改变。在如今的 Java 世界中,基于微服务的 web 应用程序能够很容易地将它们自身所需的运行时环境打包到一个可以运行的档案文件中。现代的嵌入时运行时,例如 Tomcat 和 Jetty,是它们前身的应用服务器所对应的轻量级版本,它们通常都能够做到在几秒钟之内迅速启动。所有安装了 Java 的系统都能够直接运行部署的程序,这也简化了部署新变更的流程。

Spring Boot

Spring Boot 这个框架在经历了不断的演变之后,如今已经能够用于开发 Java 微服务了。Boot 是基于 Spring 框架进行开发的,也继承了 Spring 的成熟性。它通过一些内置的固件封装了底层框架的复杂性,以帮助使用者进行微服务的开发。Spring Boot 的一大优点是提高开发者的生产力,因为它已经提供了许多通用的功能,例如 RESTful HTTP 以及嵌入式的 web 应用程序运行时,因此很容易进行装配及使用。在许多方面上,它也是一种“微框架”,允许开发者选择在整个框架中他们所需的那部分,而无需使用庞大的、或是不必要的运行时依赖。这也让 Boot 应用程序能够被打包为多个小单元以进行部署,并且该框架还能够使用构建系统生成可部署文件,例如可运行的 Java 档案包。

Spring Boot 团队提供了一种便利的机制,让开发者能够简单地上手创建应用程序,也就是所谓的 Spring Initializr。这个页面的作用是引导基于 Boot 的 web 应用程序的构件配置,并且允许开发者在多个分类中选择在项目中需要使用的类库。开发者只需要输入项目的一些元数据、选择所需的依赖项、并且单击“生成项目”按钮,就能够生成一个基于 Maven 或 Gradle 的 Spring Boot 项目的压缩文件了。文件里提供了用于开始设计项目的脚手架代码,对于首次使用这个框架的开发者来说是个绝佳的起点。

作为一个框架,Boot 中内建了一些聚合模块,通常称为“启动者”。这些启动模块中是一些类库的已知的、良好的、具备互操作性的版本的组合,这些类库能够为应用程序提供某些方面的功能。Boot 能够通过应用程序的配置对这些类库的进行设置,这也为整个开发周期中带来了配置胜于约定的便利性。这些启动模块中有许多是专门用于进行微服务架构开发的,它们为应用程序的开发者带来了一些免费的关键功能。在 Spring Boot 中实现一个基于 HTTP 的 RESTful 微服务,只需简单地加入 actuator 与 web 启动模块就足够了。web 模块将提供嵌入式的运行时,而且能够让使用者基于 RESTful HTTP 控制器进行微服务 API 的开发,而 actuator 模块则为对外暴露的试题、配置参数和内部组件的映射提供了基本功能与 RESTful HTTP 终结点,因而使微服务能够正常运转,同时也为调试提供了极大的便利。

作为一个微服务框架,Boot 的很大一部分价值在于它能够无缝地为基于 Maven 和 Gradle 的项目提供各种构建工具。通过使用 Spring Boot 插件,就能够利用该框架的能力,将项目打包为一个轻量级的、可运行的部署包,而除此之外几乎不需要进行任何额外的配置。在列表 1 中的代码展示了一个 Gradle 的构建脚本,可作为运行某个 Spring Boot 微服务的起点。此外,也可在 Spring Initializr 网站上选择使用较繁琐的 Maven POM 的示例,同时需要将应用程序的启动类的地址告诉该插件。而在使用 Gradle 时则无需进行这方面的配置,因为插件本身就能够找到这个类的地址。

复制代码
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'org.springframework.boot:spring-boot-gradle-plugin:1.2.0.RELEASE'
}
}
apply plugin: 'spring-boot'
repositories { jcenter()
}
dependencies {
compile "org.springframework.boot:spring-boot-starter-actuator"
compile "org.springframework.boot:spring-boot-starter-web"
}

列表 1 – Gradle 的构建脚本

如果选择使用 Spring Initializr 上的项目,就需要让项目结构符合常规的需求,只需遵循 Maven 风格的项目结构就能够实现这一点。代码必须被保存在 src/main/java 文件夹下,这样才能够正确地编译。该项目随后还要提供一个应用程序的入口点。在 Spring Initializr 的脚手架代码中有一个名为 DemoApplication.java 的文件,它的作用正是该项目的 main 类。可以随意对这个类进行重命名,通常来说将其命名为“Main”就可以了。列表 1.1 的示例描述了开始开发一个微服务所需的最少代码。

复制代码
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
@EnableAutoConfiguration
public class Main {
public static void main(String[] args) {
SpringApplication.run(Main.class);
}
}

列表 1.1 - Spring Boot 应用

通过在 Main 类中使用“EnableAutoConfiguration”标注,该框架就能够进行行为的配置,以引导应用程序的启动与运行。这些行为很大程度上是通过约定用于配置的方式确定的,为此 Boot 将对 classpath 进行扫描,以确定微服务中需要具备哪些功能。在上面的示例中,该微服务选择了 actuator 与 web 这两个启动模块,因此该框架能够确定这个项目是一个微服务,引导某个嵌入的 Tomcat 容器的启动,并通过某个预先配置的终结点提供该服务。在该示例中的代码并没有进行太多工作,但只需简单地启动该示例,就能够使 actuator 模块所暴露的终结点开始运行。只需将该项目导入任何 IDE,随后为“Main”类创建一个“作为 Java 应用程序运行”的配置,就能够启动这个微服务了。此外,也可以选择在命令行中运行 gradle bootRun 这个 Gradle 任务,或是针对 Maven 的 mvn spring-boot:run 命令,也能够启动该应用程序,具体的命令取决于你选择了哪种项目配置。

操作数据

接下来我们要实现之前所说的那个“产品的垂直分片”,考虑一下“产品详细信息”这个服务,它与“产品价格”这个服务一起提供了登录页面体验的详细信息。至于微服务的职责,它的数据领域应当是与某个“产品”相关的属性的子集,包括产品名称、简短描述、详细描述、以及一个库存 id。可以使用 Java bean 对这些信息进行建模,正如列表 1.2 中的代码所描述的一样。

复制代码
import javax.persistence.Entity;
import javax.persistence.Id;
@Entity
public class ProductDetail {
@Id
private String productId;
private String productName;
private String shortDescription;
private String longDescription;
private String inventoryId;
public String getProductId() {
return productId;
}
public void setProductId(String productId) {
this.productId = productId;
}
public String getProductName() {
return productName;
}
public void setProductName(String productName) {
this.productName = productName;
}
public String getShortDescription() {
return shortDescription;
}
public void setShortDescription(String shortDescription) {
this.shortDescription = shortDescription;
}
public String getLongDescription() {
return longDescription;
}
public void setLongDescription(String longDescription) {
this.longDescription = longDescription;
}
public String getInventoryId() {
return inventoryId;
}
public void setInventoryId(String inventoryId) {
this.inventoryId = inventoryId;
}
}

列表 ****1.2 —— 产品详细信息的POJO对象

在 ProductDetail 这个 Java bean 中有一点要特别注意,这个类使用了 JPA 标注,以表示它是一个实体。Spring Boot 中专门提供了一个可用于 JPA 实体与关系型数据库数据源的启动模块。考虑一下列表 1 中的构建脚本,我们可以在其中的“依赖”一节中加入这个 Boot 的启动模块,以用于持久化数据集,如列表 1.3 中的代码所示。

复制代码
dependencies {
compile "org.springframework.boot:spring-boot-starter-actuator"
compile "org.springframework.boot:spring-boot-starter-web"
compile "org.springframework.boot:spring-boot-starter-data-jpa"
compile 'com.h2database:h2:1.4.184'
}

列表 1.3 —— 在构建脚本中设置Spring Boot的依赖

出于演示与原型的目的,该项目中现在还包括了内嵌的 h2 数据库类型。Boot 的自动配置机制能够检测到 classpath 中存在 h2,随后为 ProductDetail 实体生成必要的表结构。在内部,Boot 会调用 Spring Data 进行对象实体映射操作,有了它之后,我们就可以利用它的约定和机制与数据库打交道了。Spring Data 中提供了一个便捷的抽象,也就是“repository”的概念,它本质上就是一种数据访问对象(DAO),该对象在启动时会由框架为我们自动装配。为了实现 ProductDetail 实体的 CRUD 功能,我们只需要创建一个接口,扩展在 Spring Data 中内置的 CrudRepository 即可,正如列表 1.4 中的代码所示。

复制代码
import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface ProductDetailRepository extends CrudRepository <ProductDetail, String>{
}

列表 1.4 —— 产品信息的数据访问对象(Spring Data Repository

在接口定义中的 @Repository 标注将通知 Spring,这个类的作用是一个 DAO。这个标注也是一种特别的机制,我们可以通过这种机制通知框架,让框架自动将其进行装配,并分配到微服务的配置中,从而让我们可以使用依赖注入的方式访问它。为了在 Spring 中应用这一特性,我们还必须在列表 1.1 中定义的 Main 类上添加 @ComponentScan 这个额外的标注。当微服务启动之后,Spring 将会对项目的 classpath 进行扫描以寻找各种组件,并且将这些组件作为应用程序中需要自动装配的备选组件。

为了展现微服务的新能力,请仔细阅读列表 1.5 中的代码,这里我们利用了一个先决条件,就是 Boot 会在 main() 方法中为我们提供一个指向 Spring 的 ApplicationContext 的引用。

复制代码
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.ComponentScan;
@ComponentScan
@EnableAutoConfiguration
public class Main {
public static void main(String[] args) {
ApplicationContext ctx = SpringApplication.run(Main.class);
ProductDetail detail = new ProductDetail();
detail.setProductId("ABCD1234");
detail.setProductName("Dan's Book of Writing");
detail.setShortDescription("A book about writing books.");
detail.setLongDescription("In this book about writing books, Dan will show you how to write a book.");
detail.setInventoryId("009178461");
ProductDetailRepository repository = ctx.getBean(ProductDetailRepository.class);
repository.save(detail);
for (ProductDetail productDetail : repository.findAll()) {
System.out.println(productDetail.getProductId());
}
}
}

列表 1.5 —— 展现加载数据的功能

在这个简单的示例中,我们为一个 ProductDetail 对象加载了某些数据,我们通过调用 ProductDetailRepository 的方法将产品信息进行保存,随后再次调用这个 repository 对象,从数据库中取回产品的信息。到目前为止,对于在微服务使用持久化数据,没有进行任何额外的配置。我们可以使用列表 1.5 中的这个原型代码作为定义 RESTful HTTP API 契约的基础,通过 Spring 中提供的 @RestController 标注就可以实现。

设计 API

对于“产品信息”这个微服务来说,提供简单的 CRUD 式功能或许就已经足够了,但也许它还需要提供一些扩展功能,例如分页的结果集和数据过滤。可以通过一个简单的控制器(controller)实现这个操作数据集的 API,Spring 会将该控制器映射到某个 HTTP 的路由上。下方的列表 1.6 中的代码示例可以作为一个起点,这个 API 暴露了 create 与 findAll 方法,通过它可以实现之前那个原型中所演示的代码功能。

复制代码
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/products")
public class ProductDetailController {
private final ProductDetailRepository repository;
@Autowired
public ProductDetailController(ProductDetailRepository repository) {
this.repository = repository;
}
@RequestMapping(method = RequestMethod.GET)
public Iterable findAll() {
return repository.findAll();
}
@RequestMapping(method = RequestMethod.POST)
public ProductDetail create(@RequestBody ProductDetail detail) {
return repository.save(detail);
}
}

列表 1.6 —— Product Detail**** 控制器类

Spring 中提供的 @RestController 标注将通知该框架,让框架为我们实现数据序列化与数据绑定的大部分繁重工作。此外,对于那些将为这个微服务生成数据的服务来说,我们只需为 create() 方法的参数标注为 @RequestBody,Spring 就能够自动为我们生成该对象的内容。随后就可以使用系统自动装配的 ProductDetailRepository 对象保存相应的 ProductDetail 对象。Boot 为 Spring 中内置提供的这些功能加入了一些额外的数据转换器,它们将通过 Jackson 类库,将 ProductDetail 对象序列化为 JSON 格式,以便微服务的 API 的调用者进行操作。在列表 1.6 中的控制器示例的基础上,如果该服务的 /products 终结点收到了一个 JSON 格式的请求,那么该服务就会创建一个新的产品信息项,正如列表 1.7 中所描述的那样。

复制代码
{
"productId": "DEF0000",
"productName": "MakerBot",
"shortDescription": "A product that makes other products",
"longDescription": "This is an extended description for a makerbot, which is basically a product that makes other products.",
"inventoryId": "00854321"
}

列表 1.7 —— 用于表现某个产品的JSON结构

通过对 /products 这个地址进行一个 HTTP GET 请求,可以刷新产品的详细信息,并显示新创建的产品细节内容。

在微服务的 create() 中基本上只有一个用例,就是进行数据绑定并保存到 repository 中。但在某些情况下,该服务还需要执行一些较复杂的业务逻辑,以确保保存到产品信息中的数据的准确性。我们可以通过使用 Spring 中内置的校验框架,在进行数据绑定时确认产品信息中的数据符合微服务的业务逻辑。在列表 1.8 中的代码展现了对 ProductDetail 校验逻辑的一种实现,它将调用另一个微服务中的方法,以确定所提供的库存 ID 的有效性。

复制代码
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.validation.*;
@Component
public class ProductDetailValidator implements Validator {
private final InventoryService inventoryService;
@Autowired
public ProductDetailValidator(InventoryService inventoryService) {
this.inventoryService = inventoryService;
}
@Override
public boolean supports(Class<?>clazz) {
return ProductDetail.class.isAssignableFrom(clazz);
}
@Override
public void validate(Object target, Errors errors) {
ProductDetail detail = (ProductDetail)target;
if (!inventoryService.isValidInventory(detail.getInventoryId())) {
errors.rejectValue("inventoryId", "inventory.id.invalid", "Inventory ID is invalid");
}
}
}

列表 ****1.8 ——ProductDetail的校验逻辑

这段示例代码中的 InventoryService 中的逻辑有些生硬,但不难看出这种进行数据校验的机制具有固有的灵活性,这也利益于该服务能够对其它微服务进行查询调用,以获得其它微服务对于整个数据领域中某些子数据的信息。

为了在数据绑定时能够使用 ProductDetailValidator 的功能,需要在 Spring 的数据绑定器中进行注册,而注册时机是特定于控制器的。在下方的列表 1.9 中对控制器的代码进行了改动,展现了如何在控制器中对校验器进行自动装配,并通过 initBinder() 方法将其注册进行数据绑定的过程。@InitBinder 这个标注将通过 Spring,我们将对这个类的默认数据绑定器进行自定义。此外,请注意 thecreate() 方法中的 ProductDetail 对象参数现在加上了一个 @Valid 标注,该标注的作用是通知数据绑定器,我们需要在数据绑定时对请求体进行校验。而 Spring 中内置的校验器也将提供 JSR-303 与 JSR-349 这两种数据校验规范(Bean 校验)的字段级标注。

复制代码
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
@RestController
@RequestMapping("/products")
public class ProductDetailController {
private final ProductDetailRepository repository;
private final ProductDetailValidator validator;
@Autowired
public ProductDetailController(ProductDetailRepository repository, ProductDetailValidator validator) {
this.repository = repository;
this.validator = validator;
}
@InitBinder
protected void initBinder(WebDataBinder binder) {
binder.addValidators(validator);
}
@RequestMapping(method = RequestMethod.GET)
public Iterable findAll() {
return repository.findAll();
}
@RequestMapping(method = RequestMethod.POST)
public ProductDetail create(@RequestBody @Valid ProductDetail detail) {
return repository.save(detail);
}
}

列表 ****1.9 —— 经过修改后的Product Detail控制器,现在加入了校验器

如果该 API 的调用者在 POST 提交的 JSON 结构中没有包含一个有效的库存 ID,Spring 将会产生一个校验失败的错误,并且为调用者返回一个“400 – Bad Request”的 HTTP 状态码。由于控制器的定义使用了 RestController 这个标注,因此 Spring 能够将校验失败的信息进行正确地格式化,让调用者能够理解其内容。作为这个微服务的开发者,实现这一功能无需进行任何额外的配置。

对于电子商务网站这个示例来说,一个仅包含简单的 CRUD REST API 的产品详细信息微服务没有什么太大的作用。这个服务还需要提供对产品信息结果列表进行分页以及排序的功能,并且提供某种程序上的搜索功能。为了实现第一个需求,需要对 ProductDetailController 中的 findAll() 这个控制器 action 方法进行修改,让它能够接受由 API 使用者所定义的数据范围所对应的查询参数,然后该方法就可以使用 Spring Data 中内置的 PagingAndSortingRepositorytype 类,在 findAll() 方法中对 repository 进行调用时提供分页及排序的参数。我们需要修改 ProductDetailRepository,让它继承自这个新的类型,如列表 1.10 中的代码所示。

复制代码
import org.springframework.data.repository.PagingAndSortingRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface ProductDetailRepository extends PagingAndSortingRepository<ProductDetail, String> {
}

列表 ****1.10 —— 修改后的ProductDetailRepository提供了对分页与排序的支持

列表 1.11 中的代码展现了经过修改后的 findAll 这个控制器方法,它能够利用 repository 中新的分页与排序功能。如果某个对 /products 这个终结点的 API 调用提供了?page=0&count=20 这个查询字符串,该方法就能够返回数据库中的前 20 条结果。在这个示例中的代码还利用了 Spring 的功能,为查询参数赋予了默认值,因此这些参数中的大部分都成为可选参数了。

复制代码
@RequestMapping(method = RequestMethod.GET)
public Iterable findAll(@RequestParam(value = "page", defaultValue = "0", required = false) int page,
@RequestParam(value = "count", defaultValue = "10", required = false) int count,
@RequestParam(value = "order", defaultValue = "ASC", required = false) Sort.Direction direction,
@RequestParam(value = "sort", defaultValue = "productName", required = false) String sortProperty) {
Page result = repository.findAll(new PageRequest(page, count, new Sort(direction, sortProperty)));
return result.getContent();
}

列表 ****1.11 ——ProductDetailController中修改后的findAll方法,现在能够支持分页及排序功能

当该电子商务网站的用户进行登陆页面时,该网页会通过贪婪查询方式加载 10 条或 20 条结果,随后当滚动条到达页面上的某个位置,或是经过一段时间后,通过延迟加载的方式获取之后的 50 条结果。通过这个内置的分页功能,调用者就能够控制每次调用需要返回的数据量。列表 1.12 中描述了 ProductDetailController 的完整实现。

复制代码
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.MutablePropertyValues;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.*;
import org.springframework.http.*;
import org.springframework.validation.DataBinder;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
import javax.validation.Valid;
import java.io.IOException;
@RestController
@RequestMapping("/products")
public class ProductDetailController {
private final ProductDetailRepository repository;
private final ProductDetailValidator validator;
private final ObjectMapper objectMapper;
@Autowired
public ProductDetailController(ProductDetailRepository repository, ProductDetailValidator validator,
ObjectMapper objectMapper) {
this.repository = repository;
this.validator = validator;
this.objectMapper = objectMapper;
}
@InitBinder
protected void initBinder(WebDataBinder binder) {
binder.addValidators(validator);
}
@RequestMapping(method = RequestMethod.GET)
public Iterable findAll(@RequestParam(value = "page", defaultValue = "0", required = false) int page,
@RequestParam(value = "count", defaultValue = "10", required = false) int count,
@RequestParam(value = "order", defaultValue = "ASC", required = false) Sort.Direction direction,
@RequestParam(value = "sort", defaultValue = "productName", required = false) String sortProperty) {
Page result = repository.findAll(new PageRequest(page, count, new Sort(direction, sortProperty)));
return result.getContent();
}
@RequestMapping(value = "/{id}", method = RequestMethod.GET)
public ProductDetail find(@PathVariable String id) {
ProductDetail detail = repository.findOne(id);
if (detail == null) {
throw new ProductNotFoundException();
} else {
return detail;
}
}
@RequestMapping(method = RequestMethod.POST)
public ProductDetail create(@RequestBody @Valid ProductDetail detail) {
return repository.save(detail);
}
@RequestMapping(value = "/{id}", method = RequestMethod.PUT)
public HttpEntity update(@PathVariable String id, HttpServletRequest request) throws IOException {
ProductDetail existing = find(id);
ProductDetail updated = objectMapper.readerForUpdating(existing).readValue(request.getReader());
MutablePropertyValues propertyValues = new MutablePropertyValues();
propertyValues.add("productId", updated.getProductId());
propertyValues.add("productName", updated.getProductName());
propertyValues.add("shortDescription", updated.getShortDescription());
propertyValues.add("longDescription", updated.getLongDescription());
propertyValues.add("inventoryId", updated.getInventoryId());
DataBinder binder = new DataBinder(updated);
binder.addValidators(validator);
binder.bind(propertyValues);
binder.validate();
if (binder.getBindingResult().hasErrors()) {
return new ResponseEntity<>(binder.getBindingResult().getAllErrors(), HttpStatus.BAD_REQUEST);
} else {
return new ResponseEntity<>(updated, HttpStatus.ACCEPTED);
}
}
@RequestMapping(value = "/{id}", method = RequestMethod.DELETE)
public HttpEntity delete(@PathVariable String id) {
ProductDetail detail = find(id);
repository.delete(detail);
return new ResponseEntity<>(HttpStatus.ACCEPTED);
}
@ResponseStatus(HttpStatus.NOT_FOUND)
static class ProductNotFoundException extends RuntimeException {
}
}

列表 ****1.12 —— ProductDetailController**** 的完整实现

毫无疑问,除了数据的分页与排序之外,这个电子商务网站还需要提供一些类似于搜索引擎一样的功能。由于在垂直分片中的每个微服务都对自己的数据领域子集进行维护,因此它也理应负责自身的搜索功能。这也让调用者能够异步地对整个数据领域中很大一部分的属性进行搜索。

Spring Data 允许对 repository 的接口附加某个方法签名,在其中加入自定义的查询。这表示 repository 能够使用一种预先确定的 JPA 查询,它将对每个产品信息对象中的一个属性子集进行查询,这就让微服务能够具备一些原始的搜索功能。在列表 1.13 中所描述的代码中对 ProductDetailRepository 进行了修改,其中加入了一个 search() 方法,它能够接受查询语句,并尝试对 productName 或 longDescription 字段进行大小写无关的匹配,并将一个结果列表返回给调用 者。

复制代码
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.PagingAndSortingRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface ProductDetailRepository extends PagingAndSortingRepository<ProductDetail, String> {
@Query("select p from ProductDetail p where UPPER(p.productName) like UPPER(?1) or " +
"UPPER(p.longDescription) like UPPER(?1)")
List search(String term);
}

列表 ****1.1.3 ——ProductDetailRepository中的自定义查询

为了公开这个搜索功能,我们将创建另一个 RestController,并将它映射到 /search 这个终结点,如列表 1.1.4 中所示。

复制代码
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.ArrayList;
import java.util.List;
@RestController
@RequestMapping("/search")
public class ProductDetailSearchController {
private final ProductDetailRepository repository;
@Autowired
public ProductDetailSearchController(ProductDetailRepository repository) {
this.repository = repository;
}
@RequestMapping(method = RequestMethod.GET)
public List search(@RequestParam("q") String queryTerm) {
List productDetails = repository.search("%"+queryTerm+"%");
return productDetails == null ? new ArrayList<>() : productDetails;
}
}

列表 ****1.14 —— 用于对ProductDetail进行搜索的控制器 Search controller for ProductDetails

将来还可以进一步增加这个 ProductDetailSearchController 的功能,可以让它在查询时实现与 ProductDetailController 相同的分布及排序功能。

配置

Spring Boot 中丰富的应用程序配置能够让创建的微服务具有强大的能力,而且在某些场合下完全不需要修改这些配置。当准备将服务进行部署时,也许要根据部署环境或某些外部的影响的结果对某些配置指令进行调整,例如在哪个端口上运行内嵌的容器。Boot 为微服务的开发者提供了多种方式以重写这些默认的配置,而且该框架也支持由多个不同的因素决定实际的配置。

在进行微服务的配置时,要仔细考虑某个重要的因素,即该服务的运行时环境。如果服务是部署在某个静态的基础设施中,那么对某些配置项进行预定义或许是可行的。为了更加清晰地说明这个问题,再来看看上面的那个示例,这个微服务的数据源只是一个简单的内嵌的 h2 数据库。而在生产环境中,该微服务将指向某个持久化的数据源,例如某个 MySQL 或 Oracle 数据库,因此应用程序的配置必须包含正确的 JDBC URL、用户名、密码,并且使用适当的 JDBC 驱动类。在静态基础设施中,可以对这些配置进行预定义,并直接打包在应用程序中。而 Boot 自身就提供了从 Java 属性文件、XML 配置文件或 YAML 配置文件中解析配置的功能,并相应地在 classpath 的根目录中寻找名为 application.properties、application.xml 或 application.yml(或是 application.yaml)的配置文件。列表 1.15 中的配置文件展现了如何使用配置指令覆盖默认的数据源配置。

复制代码
spring.datasource.url=jdbc:mysql://prod-mysql/product
spring.datasource.username=root
spring.datasource.password=
spring.datasource.driver-class-name=com.mysql.jdbc.Driver

列表 ****1.15 —— 在配置文件中指定数据源

Boot 中的配置机制有一个很重要的能力,它能够在启动时使用 Java 系统属性对配置进行重写。在 JVM 启动中提供的配置将覆盖在 classpath 中指定的 application.properties 中的内容。这就说明运行时环境能够基于某些在对微服务进行打包时未知的信息对配置进行自定义。举例来说,如果该微服务运行在某个非静态环境,例如云端部署环境中,那么也许要根据 VM 或容器的地点来决定数据库的托管。应用程序可以通过系统环境变量访问这些信息。可以通过 JVM 的启动参数,或是直接在配置中方便地调用这些环境变量。在后一种方式里,可以使用 Spring 中的属性占位符获取某个配置指令的引用。在列表 1.16 中展现的配置文件对列表 1.15 进行了一些修改,它使用了属性占位符,并且包含一个默认值。

复制代码
spring.datasource.url=${JDBC_URL:jdbc:mysql://prod-mysql/product}
spring.datasource.username=${JDBC_USER:root}
spring.datasource.password=${JDBC_PASS:}
spring.datasource.driver-class-name=com.mysql.jdbc.Driver

列表 ****1.16 —— 修改后的配置文件使用了带有默认值的系统变量

Boot 也将对文件系统进行查找,以寻找一个相对于启动路径的,名为“config”的目录,并在其中寻找与之前所述相同的一系列配置文件,如果一旦找到,那么在应用 classpath 中找到的任何配置之前,它会首先应用这些配置。而 spring.config.location 这个 Java 系统属性也能够告诉 Spring 配置文件的地址。比方说,如果微服务的配置文件地址是 /etc/spring/boot.yml,那么通过 –D spring.config.location=/etc/spring/boot.yml 指令,其中的配置就能够覆盖文件系统中的配置信息。通过同样的方式也可以使用 classpath 中定义的资源,只需在属性的值前面加上 classpath: 前缀即可。

利用同样的配置机制,通过 server.port 这个键,还可以自定义内嵌的容器的服务器端口。如果在某种 PaaS 云端环境,例如 Heroku 中运行微服务时,这一点尤为重要。这个键将通过某个环境变量映射端口的范围,并将其暴露给应用程序。在列表 1.16 中的配置指令也能够用于映射 PORT 环境变量。列表 1.17 就展现了这种配置的方式。

复制代码
server.port=${PORT:8080}

列表 ****1.17 —— 将启动端口映射为某个环境变量的配置 Configuration to map the startup port to an environment var

打包

当微服务的部署已经准备就绪后,就可以使用 Boot 中提供的构建系统的工具,以生成一个轻量级的、可运行的部署文件。正如本文之前所说的一样,Boot 为 Gradle 和 Maven 提供了插件,因此可以通过它们创建一个可运行的 JAR 文件用于发布。只需使用在前文中提到的 Gradle 构建脚本,就能够通过调用项目的构造任务 gradle build,简单地生成 JAR 文件。Boot 将对 jar 任务进行拦截,并将常规方式生成的文件进行重写打包,在其中加入所有的依赖项,以生成所谓的“fat”或“uber”JAR 文件。而在 Maven 项目的配置中,Boot 插件也能够拦截打包过程,并进行相同的重新打包操作。

Boot 的 Gradle 插件还有一个额外的优点,就是它能够与应用程序的插件进行交互,它将生成一个可发布的 tarball 文件,其中已经重新打包了所有依赖,以及在多种 Unix 及 Windows 上的启动脚本。这种打包方式对于部署来说非常理想,因为微服务中所有的启动脚本都已经写入包里了。只需在目标服务器上将 tarball 解压缩,就能够在 bin 文件夹中以项目名称命名的脚本中直接启动微服务了。

虽然对于微服务来说,使用单一的可部署文件是受推荐的方式,也是一种接受度最高的可部署单元,但绝不是说必须强制使用单一的部署文件。Boot 的功能更进一步,它能够将应用程序打包为 WAR 文件,并部署到某个应用程序容器中。为了使用 war 插件,需要对 Gradle 的构建脚本进行一些修改,如列表 1.18 所示。与之前的示例相似,该构建任务将生成 web 文件。

复制代码
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'org.springframework.boot:spring-boot-gradle-plugin:1.2.0.RELEASE'
}
}
apply plugin: 'spring-boot'
apply plugin: 'war'
repositories {
jcenter()
}
dependencies {
compile "org.springframework.boot:spring-boot-starter-actuator"
compile "org.springframework.boot:spring-boot-starter-web"
compile "org.springframework.boot:spring-boot-starter-data-jpa"
compile 'mysql:mysql-connector-java:5.1.34'
}

列表 ****1.18 —— Gradle的构建脚本,使用了Boot以及War**** 插件

在 Maven 项目中,可以通过修改项目的 pom.xml 文件中的打包配置实现 war 打包。列表 1.19 中的片段展现了经过修改后的配置。

复制代码
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.infoq</groupId>
<artifactId>sb-microservices</artifactId>
<version>0.1.0</version>
<packaging>war</packaging>
<!-- ...remaining omitted for brevity... -->
</project>

列表 ****1.19 —— 可以用于War打包的Maven pom.xml的起始部分代码

网关 API

在之前的章节中,我们已经深入地探讨了产品信息这个微服务的开发,这套模板同样也能够以相似的方式应用于这个电子商务网站的垂直分片中的其它每一种服务中。当系统的每个垂直分片中的各个组件被分解为一系列微服务的集合之后,该系统就可被视为一种完全分布式的微服务架构。但对于外部调用者,例如该电子商务网站的页面来说,这种方式也会产生一些复杂性,因为这些调用者从系统中获取的数据可能会横跨多个不同的微服务。如果不通过某种机制,将这些服务综合地重新组织为一种看起来具有整体性的 API,那么每个客户端的调用者将不得不承担这种职责,它们将分别调用这些不同的数据集,并将它们重新组织为一种可重用的结构。这种方式对于调用者产生了很大的负责,因为他们必须建立大量的 HTTP 连接,以实现对某些数据集的聚合。这也意味着如果某个服务不可用或掉线,那么每个调用者将负责对数据缺失这一场景进行适当的处理。

用于微服务基础设施中的某种模式正在逐渐浮现,这种模式体现了一种网关 API 服务的概念,这种服务处于各个不同的后端服务的前方,并为调用者提供一种全面的、易于使用的 API。继续以电子商务网站的例子进行讲解,当网站的某个访问者打算查看某个产品的详细信息时,为了生成产品信息视图的数据,需要四种服务参与其中。该网页不再对这些服务分别进行调用,而是访问该网关服务的某个聚合 API 的终结点,网关服务会代为调用底层服务,并将结果集进行合并,返回页面进行显示。从网页的角度来看,它只是发送了一个调用请求,就获得了显示页面所必须的完整数据。

这种方式还具有一个额外的好处,就是可以在调用者与后台服务之间更好地管理数据的传输。比方说,该网关服务在它的服务层可以实现某些逻辑,当对某个产品的信息的访问量很大的时候,它将不会在每个请求中都去调用相应的产品信息微服务,而是选择在某个预定义的时间段之内直接返回缓存中的数据。这种方式能够显著地提升性能,并减少网络负载。

同样重要的一点还在于对后台服务的可用性进行抽象。一旦发生某个后台服务不可用的情况下,网关服务能够明智地决定应该提供怎样的数据。现实这一点的方式有多种,而在网关服务中确保分布式系统的持久性这方面最引人注目的一种机制是由 Netflix 开发的名为 Hystrix 的类库。在 Hystrix 中有许多功能能够确保对故障的适应性,并且对于海量请求提供了性能方面的优化,但其中最吸引人的特性大概要数它对断路设计模式的实现了。具体来说,Hystrix 能够观察到对某个后端服务的连接断开,在这种情况下它不会选择持续访问这个下线的服务,因为这会造成网络阻塞以及等待超时,而是打开这个服务的回路,将后续的请求委托给某个“后备”方法,让它接管这些调用。在底层,Hystrix 会间隔式地检查该连接,查看该后台服务是否已经恢复了正常操作状态,如果服务已经恢复,那么它就会重新建立通信连接。

当回路打开的期间,网关服务能够任意选择返回给调用者的响应。可以使用某些“最后一次正常运行时”的正确数据集,也可以返回一个空响应,在头信息中告知调用者后端回路已经打开,或是以上两者的某种结合。Hystrix 提供的适应性在任何一个具有一定复杂性的分布式系统中都是一种关键的组件。为了更直白地理解 Hystrix 的能力,我们再回头来看看这个电子商务中的产品垂直分片,其中必须调用四种服务,以获得产品信息视图中对应的数据。列表 1.20 展现了使用网关 API 服务的 ProductService 的代码。

复制代码
import com.netflix.hystrix.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import java.util.*;
import java.util.concurrent.*;
@Service
public class ProductService {
private static final String GROUP = "products";
private static final int TIMEOUT = 60000;
private final ProductDetailService productDetailService;
private final ProductPricingService productPricingService;
private final ProductRatingService productRatingService;
private final ProductReviewService productReviewService;
@Autowired
public ProductService(ProductDetailService productDetailService, ProductPricingService productPricingService,
ProductRatingService productRatingService, ProductReviewService productReviewService) {
this.productDetailService = productDetailService;
this.productPricingService = productPricingService;
this.productRatingService = productRatingService;
this.productReviewService = productReviewService;
}
public Map<String, Map<String, Object>> getProductSummary(String productId) {
List<Callable<AsyncResponse>> callables = new ArrayList<>();
callables.add(new BackendServiceCallable("details", getProductDetails(productId)));
callables.add(new BackendServiceCallable("pricing", getProductPricing(productId)));
return doBackendAsyncServiceCall(callables);
}
public Map<String, Map<String, Object>> getProduct(String productId) {
List<Callable<AsyncResponse>> callables = new ArrayList<>();
callables.add(new BackendServiceCallable("details", getProductDetails(productId)));
callables.add(new BackendServiceCallable("pricing", getProductPricing(productId)));
callables.add(new BackendServiceCallable("ratings", getProductRatings(productId)));
callables.add(new BackendServiceCallable("reviews", getProductReviews(productId)));
return doBackendAsyncServiceCall(callables);
}
private static Map<String, Map<String, Object>> doBackendAsyncServiceCall(List<Callable<AsyncResponse>> callables) {
ExecutorService executorService = Executors.newFixedThreadPool(4);
try {
List<Future<AsyncResponse>> futures = executorService.invokeAll(callables);
executorService.shutdown();
executorService.awaitTermination(TIMEOUT, TimeUnit.MILLISECONDS);
Map<String, Map<String, Object>> result = new HashMap<>();
for (Future<AsyncResponse> future : futures) {
AsyncResponse response = future.get();
result.put(response.serviceKey, response.response);
}
return result;
} catch (InterruptedException|ExecutionException e) {
throw new RuntimeException(e);
}
}
@Cacheable
private HystrixCommand<Map<String, Object>> getProductDetails(String productId) {
return new HystrixCommand<Map<String, Object>>(
HystrixCommand.Setter
.withGroupKey(HystrixCommandGroupKey.Factory.asKey(GROUP))
.andCommandKey(HystrixCommandKey.Factory.asKey("getProductDetails"))
.andCommandPropertiesDefaults(
HystrixCommandProperties.Setter()
.withExecutionIsolationThreadTimeoutInMilliseconds(TIMEOUT)
)
) {
@Override
protected Map<String, Object> run() throws Exception {
return productDetailService.getDetails(productId);
}
@Override
protected Map getFallback() {
return new HashMap<>();
}
};
}
private HystrixCommand<Map<String, Object>> getProductPricing(String productId) {
// ... snip, see getProductDetails() ...
}
private HystrixCommand<Map<String, Object>> getProductRatings(String productId) {
// ... snip, see getProductDetails() ...
}
private HystrixCommand<Map<String, Object>> getProductReviews(String productId) {
// ... snip, see getProductDetails() ...
}
private static class AsyncResponse {
private final String serviceKey;
private final Map<String, Object> response;
AsyncResponse(String serviceKey, Map<String, Object> response) {
this.serviceKey = serviceKey;
this.response = response;
}
}
private static class BackendServiceCallable implements Callable<AsyncResponse> {
private final String serviceKey;
private final HystrixCommand<Map<String, Object>> hystrixCommand;
public BackendServiceCallable(String serviceKey, HystrixCommand<Map<String, Object>> hystrixCommand) {
this.serviceKey = serviceKey;
this.hystrixCommand = hystrixCommand;
}
@Override
public AsyncResponse call() throws Exception {
return new AsyncResponse(serviceKey, hystrixCommand.execute());
}
}
}

列表 ****1.20 – 某个异步网关API服务的示例,其中使用了 ****Hystrix

以上示例中的服务可以用于 RESTful HTTP 客户端,这些客户端可以基于 Spring 中的 RestTemplate 进行创建,或是使用其它的某种 HTTP 客户框架,例如 Retrofit。getProductSummary() 方法对后端服务发起了一个异步调用,该服务将用于获取登陆页面所需的产品信息。与之类似,getProduct() 方法将从所有相关的后端服务中获取某个产品的详细信息,并将信息进行合并,以便 API 的调用者使用。在这个示例中,产品的信息很少会发生变化,该网关服务也应该尽量减少对于后端服务的调用次数,因此 getProductDetails() 方法利用了 Spring 提供的 @Cacheable 标注,在一段合理的时间之内对调用进行缓存。网关服务随后将通过某个映射到 /products 路由上的 RestController 获取综合的产品信息。对于微服务架构中的其它垂直分片,也可以采用类似的方式设计终结点,系统 API 的调用者也能够以一种在更传统的整体性应用程序中相同的方式来访问新的终结点。

Spring Boot 很早就意识到将整体性服务分解为分布式微服务所带来的优点,它的设计宗旨是让开发与创建微服务成为一种节省资源的,专注于开发者的流程。通过框架中所提供的启动模块以启用自动配置机制,应用程序就能够方便地充分利用强大的功能子集,否则开发者将不得不进行明确的配置,并通过编程方式进行组装。这些自动配置的模块可以作为开发一个完整的微服务架构的基础,其中已经内置了一种网关 API 服务。

关于作者

Daniel Woods是一位技术狂热者,尤其是在企业级的 Java、Groovy,和 Grails 开发方面。他在 JVM 软件开发方面已经具有超过十年以上的经验,并且通过对 Grails Ratpack web 框架这样的开源项目进行贡献的方式分享他的经验。Dan 也是 Gr8conf 和 SpringOne 2GX 会议上的演讲者,他在会议上展现了他在 JVM 的企业级应用架构上的专业知识。

查看英文原文: Building Microservices with Spring Boot

2015-05-18 13:2017191
用户头像

发布了 428 篇内容, 共 162.3 次阅读, 收获喜欢 33 次。

关注

评论

发布
暂无评论
发现更多内容

《Redis 核心技术与实战》学习笔记 07

escray

redis 学习 极客时间 3月日更 Redis 核心技术与实战

中国最可靠的 Go 模块代理

happlyfox

学习 3月日更

前端开发:数据处理方法分享(其一)

三掌柜

vue.js 大前端 3月日更

(Day30) 谁来驱动变革

mtfelix

28天写作 bewriting

【Axure9百例NO.45】中继器的不同场景下的样式处理

zhuchuanming

原型设计 Axure 交互原型

LeetCode题解:126. 单词接龙 II,BFS,JavaScript,详细注释

Lee Chen

算法 大前端 LeetCode

uni-app跨端开发H5、小程序、IOS、Android(七):uni-app渲染

黑马腾云

html5 uni-app 大前端 3月日更

领跑行业!浪潮云斩获“2021云管和云网大会”多项殊荣

浪潮云

云计算 云原生

深入理解ES8的新特性SharedArrayBuffer

程序那些事

JavaScript ecmascript 程序那些事 es8

「产品经理训练营」作业 06:用户路径地图与漏斗模型

狷介

产品经理训练营

如何将多个目录下的文件汇总到一个文件夹

IT蜗壳-Tango

3月日更 IT蜗壳教学

产品经理能力不够,产品方法论来凑

lenka

3月日更

一个有情怀的PPT模板下载网站

happlyfox

学习 3月日更 工具分享

LiteOS内核源码分析:位操作模块

华为云开发者联盟

LiteOS 源代码 位操作 bit Huawei LiteOS

图像视频压缩:深度学习,有一套

华为云开发者联盟

深度学习 自编码器 图像压缩 循环神经网络 视频压缩

时间复杂度总结

我是程序员小贱

3月日更

Kubernetes认证管理员

云原生

Kubernetes 云原生 k8s CKA

Python API 邮件发送测试

HoneyMoose

面试拜佛保过?圈内罕见阿里面试官手册,2021最强面试笔记非它莫属

Java 程序员 架构 面试

开启Python学习之旅,分享学习路上的神器集合!

王小王-123

Python 学习 资源分享 工具分享

C语言性能优化:减少相关性依赖,利用指令并行提升性能

1

编程 性能优化 C语言 cpu 100%

开源的 Switch 模拟器——GitHub 热点速览 v.21.12

HelloGitHub

GitHub 开源

NA公链(Nirvana)NAC公链独步公链江湖

区块链第一资讯

区块链

渣硕试水字节跳动,本以为简历都过不了,123+HR面直接拿到意向书

Java 程序员 架构 面试

2021春招Java后端开发面试总结:25个技术专题(最全面试攻略)

比伯

Java 编程 架构 面试 程序人生

区块链圈频现百万元年薪招聘 现金+股票仍难觅良才

CECBC

区块链人才

区块链下乡

CECBC

区块链

纸币会消失吗:数字货币如何走进我们生活

CECBC

货币

Python-计算机视觉-OpenCV-video

Aldeo

Python OpenCV Video

Wireshark数据包分析学习笔记Day19

穿过生命散发芬芳

Wireshark 数据包分析 3月日更

金三银四,GitHub 热门面试在这里

GitHub指北

使用Spring Boot创建微服务_架构_Dan Woods_InfoQ精选文章