2023年4月

本文从概念上介绍 Java 虚拟机内存的各个区域,讲解这些区域的作用、服务对象以及其中可能产生的问题。

Java 虚拟机在执行 Java 程序的过程中会把它所管理的内存划分为若干个不同的数据区域。这些区域有各自的用途,以及创建和销毁的时间,有些区域随着虚拟机进程的启动而一直存在,有些区域则是依赖用户线程的启动和结束而建立和销毁。

根据《Java 虚拟机规范》的规定, Java 虚拟机所管理的内存将会包括以下几个运行时数据区域:程序计数器、Java 虚拟机栈、本地方法栈、Java 堆、方法区。

image-20230221174739144.png

程序计数器

程序计数器(Program Counter Register)是一块较小的内存空间,程序计数器可以看作是当前线程所执行的字节码的行号指示器。在 Java 虚拟机的概念模型里,字节码解释器工作时就是通过改变程序计数器的值来选取下一条需要执行的字节码指令,程序计数器是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖程序计数器来完成。

“概念模型”这个词会经常被提及,它代表了所有虚拟机的统一外观,但各款具体的 Java 虚拟机并不一定要完全照着概念模型的定义来进行设计,具体的 Java 虚拟机可能会通过一些更高效率的等价方式去实现它。


由于 Java 虚拟机的多线程是通过线程轮流切换、分配处理器执行时间的方式来实现的,在任何一个确定的时刻, 一个处理器(对于多核处理器来说是一个内核)都只会执行一个线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每个线程都需要有一个独立的程序计数器,各个线程之间的程序计数器互不影响,独立存储,我们称这类内存区域为 “线程私有” 的内存。

如果线程正在执行的是一个 Java 方法, 程序计数器记录的是正在执行的虚拟机字节码指令的地址; 如果线程正在执行的是本地(Native) 方法,程序计数器值则应为空(Undefined)。

程序计数器内存区域是唯一一个在《Java 虚拟机规范》中没有规定任何 OutOfMemoryError 情况的区域。

Java 虚拟机栈

Java 虚拟机栈(Java Virtual Machine Stack)与程序计数器一样,也是线程私有的内存区域,Java 虚拟机栈的生命周期与线程相同。

Java 虚拟机栈描述的是 Java 方法执行的线程内存模型:每个方法被执行的时候,Java 虚拟机都会同步创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态连接、方法出口等信息。每个方法被调用直至执行完毕的过程,就对应着一个栈帧在 Java 虚拟机栈中从入栈到出栈的过程。

每一个栈帧中分配多少内存基本上是在类结构确定下来时就已知的(尽管在运行期会由即时编译器进行一些优化, 但在基于概念模型的讨论里,大体上可以认为是编译期可知的)


局部变量表

局部变量表存放了编译期可知的各种 Java 虚拟机基本数据类型(boolean、 byte、 char、 short、 int、float、 long、 double) 、对象引用(reference 类型,对象引用并不等同于对象本身,对象引用可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或者其他与此对象相关的位置)和 returnAddress 类型(指向了一条字节码指令的地址)。

这些数据类型在局部变量表中的存储空间以局部变量槽(Slot)来表示, 其中 64 位长度的 long 和 double 类型的数据会占用两个变量槽,其余的数据类型只占用一个变量槽。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时, 这个方法需要在栈帧中分配多大的局部变量空间是完全确定的,在方法运行期间局部变量表的大小不会改变。

请读者注意,这里说的 “大小” 指的是变量槽的数量,虚拟机真正使用多大的内存空间(譬如按照 1 个变量槽占用 32 个比特、 64 个比特, 或者更多)来实现一个变量槽,这是完全由具体的虚拟机实现自行决定的事情。


在《Java 虚拟机规范》中, 对 Java 虚拟机栈内存区域规定了两类异常状况:StackOverflowError、OutOfMemoryError

  • 如果线程请求的栈深度大于虚拟机所允许的深度, 将抛出 StackOverflowError 异常(栈深度溢出异常);
  • 如果 Java 虚拟机栈容量可以动态扩展,当栈扩展时无法申请到足够的内存会抛出 OutOfMemoryError 异常。

通过参数 -Xss 来设定单个线程栈的大小,栈的大小直接决定了函数调用的最大深度。

HotSpot 虚拟机的栈容量是不可以动态扩展的,以前的 Classic 虚拟机倒是可以。所以在 HotSpot 虚拟机上是不会由于虚拟机栈无法扩展而导致 OutOfMemoryError 异常。只要线程申请栈空间成功了就不会有 OOM,但是如果线程申请栈空间失败了,仍然是会出现 OOM 异常的。

本地方法栈

本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用非常相似,它们两个的区别是:虚拟机栈为虚拟机执行 Java 方法(也就是字节码) 服务,而本地方法栈则是为虚拟机使用到的本地(Native)方法服务。

《Java 虚拟机规范》对本地方法栈中方法使用的语言、使用方式与数据结构并没有任何强制规定,因此具体的虚拟机可以根据需要自由实现它,甚至有的 Java 虚拟机(譬如 HotSpot 虚拟机) 直接就将本地方法栈和虚拟机栈合二为一。

与虚拟机栈一样,本地方法栈也会在栈深度溢出或者栈扩展失败时分别抛出 StackOverflowError 和 OutOfMemoryError 异常。

Java 堆

Java 堆是一块被所有线程共享的内存区域,Java 堆在虚拟机启动时被创建。

Java 堆内存区域的唯一目的就是存放对象实例,Java 世界里 “几乎” 所有的对象实例都在 Java 堆分配内存。

在《Java 虚拟机规范》中对 Java 堆的描述是:“所有的对象实例以及数组都应当在堆上分配”,而这里笔者写的“几乎”是指从实现角度来看,随着 Java 语言的发展,现在已经能看到些许迹象表明日后可能出现值类型的支持,即使只考虑现在,由于即时编译技术的进步,尤其是逃逸分析技术的日渐强大,栈上分配、标量替换优化手段已经导致一些微妙的变化悄然发生,所以说 Java 对象实例都分配在堆上也渐渐变得不是那么绝对了。

根据《Java 虚拟机规范》的规定,Java 堆可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为连续的,这点就像我们用磁盘空间去存储文件一样,并不要求每个文件都连续存放。但对于大对象(典型的如数组对象),多数虚拟机实现出于实现简单、存储高效的考虑,很可能会要求连续的内存空间。


Java 堆既可以被实现成固定大小的,也可以是可扩展的,不过当前主流的 Java 虚拟机都是按照可扩展来实现的(通过参数 -Xmx 和 -Xms 设定)。 如果 Java 堆无法满足新的内存分配需求,并且堆也无法再扩展时,Java 虚拟机将会抛出 OutOfMemoryError 异常。

固定大小的 Java 堆指的是:只在虚拟机启动时,向操作系统申请固定大小的堆内存空间。

可扩展的 Java 堆指的是:在虚拟机启动时,向操作系统申请固定大小的初始堆内存空间。在空闲的 Java 堆内存空间无法满足新的内存分配需求时,再向操作系统申请堆内存空间。

方法区

方法区(Method Area)与 Java 堆一样, 也是被所有线程共享的内存区域。

方法区用于存储已被虚拟机加载的类型信息(如类名、访问修饰符、字段描述、方法描述等)、常量、静态变量、即时编译器编译后的代码缓存等数据。

虽然《Java 虚拟机规范》中把方法区描述为堆的一个逻辑部分,但是方法区它却有一个别名叫作 “非堆”(Non-Heap) ,目的是与 Java 堆区分开来。

根据《Java 虚拟机规范》的规定,如果方法区无法满足新的内存分配需求时,Java 虚拟机将会抛出 OutOfMemoryError 异常。


《Java 虚拟机规范》对方法区的约束是非常宽松的,除了和 Java 堆一样不需要连续的内存和可以选择固定大小或者可扩展外,甚至还可以选择不实现垃圾收集。相对而言,垃圾收集行为在方法区这个区域的确是比较少出现的,但并非数据进入了方法区就如永久代的名字一样 “永久” 存在了。方法区这个区域的内存回收目标主要是针对常量池的回收和对类型的卸载, 一般来说方法区这个区域的回收效果比较难令人满意,尤其是类型的卸载,条件相当苛刻,但是方法区这个区域的回收有时又确实是必要的。 以前 Sun 公司的 Bug 列表中,曾出现过的若干个严重的 Bug 就是由于低版本的 HotSpot 虚拟机对方法区这个区域未完全回收而导致内存泄漏。

永久代

说到方法区,不得不提一下 “永久代” 这个概念,尤其是在 JDK8 以前,许多 Java 程序员都习惯在 HotSpot 虚拟机上开发、部署程序,很多人都更愿意把方法区称为 “永久代”(Permanent Generation),或者将这两者(方法区、永久代)混为一谈。
本质上这两者(方法区、永久代)并不是等价的
,因为仅仅是当时的 HotSpot 虚拟机设计团队选择把垃圾收集器的分代设计扩展至方法区,或者说
使用永久代来实现方法区
而已, 这样使得 HotSpot 的垃圾收集器能够像管理 Java 堆一样管理方法区这部分内存,省去专门为方法区编写内存管理代码的工作。但是
对于其他的虚拟机实现, 譬如 BEA JRockit、IBM J9 等来说,是不存在永久代这个概念的


原则上如何实现方法区属于虚拟机的实现细节,不受《Java 虚拟机规范》管束, 并不要求统一。但现在回头来看,当年使用永久代来实现方法区的决定并不是一个好主意,这种设计导致了 Java 应用更容易遇到内存溢出的问题(永久代有 -XX:MaxPermSize 的上限,即使不设置也有默认大小,而 J9 和 JRockit 只要没有触碰到进程可用内存的上限, 例如32位系统中的4GB限制, 就不会出问题) ,而且有极少数方法(例如String::intern()) 会因永久代的原因而导致不同虚拟机下有不同的表现。

当 Oracle 收购 BEA 获得了JRockit 的所有权后, 准备把 JRockit 中的优秀功能,譬如 Java Mission Control 管理工具, 移植到 HotSpot 虚拟机时,但因为两者对方法区实现的差异而面临诸多困难。

考虑到 HotSpot 未来的发展,在 JDK6 的时候 HotSpot 开发团队就有放弃永久代,逐步改为采用本地内存(Native Memory)来实现方法区的计划了,到了 JDK7 的 HotSpot,已经把原本放在永久代的字符串常量池、静态变量等移出, 而到了 JDK8 , 终于完全废弃了永久代的概念, 改用与 JRockit、J9 一样在本地内存中实现的元空间(Metaspace)来代替,把 JDK7 中永久代还剩余的内容(主要是类型信息) 全部移到元空间中。

运行时常量池

运行时常量池(Runtime Constant Pool)是方法区的一部分。

Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池表(Constant Pool Table),常量池表用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。

Java 虚拟机对于 Class 文件每一部分(自然也包括常量池)的格式都有严格规定,如每一个字节用于存储哪种数据都必须符合规范上的要求才会被虚拟机认可、加载和执行,但对于运行时常量池,《Java 虚拟机规范》并没有做任何细节的要求,不同提供商实现的虚拟机可以按照自己的需要来实现这个内存区域,不过一般来说,除了保存 Class 文件中描述的符号引用外,还会把由符号引用翻译出来的直接引用也存储在运行时常量池中。

运行时常量池相对于 Class 文件常量池的另外一个重要特征是具备动态性,Java 语言并不要求常量一定只有编译期才能产生,也就是说,并非预置入 Class 文件中常量池的内容才能进入方法区的运行时常量池, 运行期间也可以将新的常量放入池中,这种特性被开发人员利用得比较多的便是 String 类的 intern() 方法。

直接内存

直接内存(Direct Memory)并不是虚拟机运行时数据区域的一部分,也不是《Java 虚拟机规范》中定义的内存区域。但是这部分内存也被频繁地使用,而且也可能导致 OutOfMemoryError 异常出现,所以我们放到这里一起讲解。

在 JDK1.4 中新加入了 NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式, NIO 它可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆里面的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆中来回复制数据。

显然,本机直接内存的分配不会受到 Java 堆大小的限制,但是,既然是内存,则肯定还是会受到本机总内存(包括物理内存、SWAP 分区或者分页文件)大小以及处理器寻址空间的限制,一般服务器管理员配置虚拟机参数时,会根据实际内存去设置 -Xmx 等参数信息,但经常忽略掉直接内存,使得各个内存区域总和大于物理内存限制(包括物理的和操作系统级的限制),从而导致动态扩展时出现 OutOfMemoryError 异常。

总结

运行时数据区域

程序计数器

程序计数器是一块较小的内存空间。程序计数器是“线程私有”的数据区域。

如果一个线程正在执行的是一个 Java 方法, 程序计数器记录的是正在执行的虚拟机字节码指令的地址。

在 Java 虚拟机的概念模型里,字节码解释器工作时就是通过改变程序计数器的值来选取下一条需要执行的字节码指令,程序计数器是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖程序计数器来完成。


Java 虚拟机栈、本地方法栈

HotSpot 虚拟机将本地方法栈和虚拟机栈合二为一。

  • Java 虚拟机栈描述的是 Java 方法执行的线程内存模型:每个方法被执行的时候,Java 虚拟机都会同步创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态连接、方法出口等信息。每个方法被调用直至执行完毕的过程,就对应着一个栈帧在 Java 虚拟机栈中从入栈到出栈的过程。
  • 本地方法栈(Native Method Stacks) 与虚拟机栈所发挥的作用非常相似,它们两个的区别是:虚拟机栈为虚拟机执行 Java 方法(也就是字节码) 服务,而本地方法栈则是为虚拟机使用到的本地(Native)方法服务。


【Java 堆】
内存区域的唯一目的就是存放对象实例,Java 世界里 “几乎” 所有的对象实例都在【Java 堆】区域分配内存。

【方法区】
内存区域用于存储已被虚拟机加载的类型信息(如类名、访问修饰符、字段描述、方法描述等)、常量、静态变量、即时编译器编译后的代码缓存等数据。

“线程私有” 的区域

“线程私有” 的内存区域:每个线程都有一个独立的内存区域,各个线程之间的内存区域互不影响, 独立存储,我们称这类内存区域为 “线程私有” 的内存区域。

  • “线程私有” 的内存区域有:程序计数器、Java 虚拟机栈、本地方法栈;
  • 被所有线程共享的内存区域有:Java 堆、方法区。

垃圾收集的区域

程序计数器、Java 虚拟机栈、本地方法栈这三个运行时数据区域随线程而生,随线程而灭,栈中的栈帧随着方法的进入和退出而有条不紊地执行着入栈和出栈操作。每一个栈帧中分配多少内存基本上是在类结构确定下来时就已知的(尽管在运行期会由即时编译器进行一些优化,但在基于概念模型的讨论里,大体上可以认为是编译期可知的),因此这三个运行时数据区域的内存分配和回收都具备确定性,在这三个运行时数据区域内就不需要过多考虑如何回收的问题,当方法结束或者线程结束时,内存自然就跟随着回收了。

而 Java 堆和方法区这两个运行时数据区域则有着很显著的不确定性:一个接口的多个实现类需要的内存可能会不一样, 一个方法所执行的不同条件分支所需要的内存也可能不一样,只有处于运行期间,我们才能知道程序究竟会创建哪些对象,创建多少个对象,这部分(Java 堆、方法区)内存的分配和回收是动态的。垃圾收集器所关注的正是这部分(Java 堆、方法区)内存该如何管理。

内存区域的异常状况

【程序计数器】内存区域是唯一一个在《Java 虚拟机规范》中没有规定任何 OutOfMemoryError 情况的区域。


【Java 虚拟机栈】、【本地方法栈】内存区域
:在【Java 虚拟机栈】、【本地方法栈】内存区域中,可能出现的异常状况有:OutOfMemoryError、StackOverflowError:

  • 创建线程时,需要申请栈空间。如果线程申请栈空间失败了,那么 Java 虚拟机就会抛出 OutOfMemoryError 异常。
  • 线程申请栈空间成功后,如果线程请求的栈深度大于虚拟机所允许的深度,那么 Java 虚拟机就会抛出 StackOverflowError 异常。


【Java 堆】内存区域
:如果 Java 堆无法满足新的内存分配需求,并且堆也无法再扩展时,Java 虚拟机将会抛出 OutOfMemoryError 异常。

【方法区】内存区域
:如果方法区无法满足新的内存分配需求时,Java 虚拟机将会抛出 OutOfMemoryError 异常。

【直接内存】
:如果各个内存区域的总和大于物理内存限制(包括物理的和操作系统级的限制),Java 虚拟机将会抛出 OutOfMemoryError 异常。

参考资料

《深入理解 Java 虚拟机》第 2 章:Java 内存区域与内存溢出异常 2.2 运行时数据区域

Thanos 简介

Thanos 是一个「开源的,高可用的 Prometheus 系统,具有长期存储能力」。很多知名公司都在使用 Thanos,也是 CNCF 孵化项目的一部分。

Thanos 的一个主要特点就是通过使用对象存储(比如 S3)可以允许 “无限” 存储空间。对象存储可以是每个云提供商提供的对象存储也可以是 ceph、rook 或 minio 这样的解决方案。

工作原理

Thanos 和 Prometheus 并肩作战,从 Prometheus 开始升级到 Thanos 是很常见的。

Thanos 被分成几个组件,每个组件都只有一个目标(典型云原生架构),组件之间通过 gRPC 进行通信。

Thanos Sidecar

Thanos 和 Prometheus 一起运行(有一个边车),每 2 小时向一个对象存储库输出 Prometheus 指标。这使得 Prometheus 几乎是无状态的。Prometheus 仍然在内存中保存着 2 个小时的度量值,所以在发生宕机的情况下,你可能仍然会丢失 2 个小时的度量值(这个问题应该由你的 Prometheus 设置来处理,使用 HA/分片,而不是 Thanos)。


每一周,我们的同事都会向社区的成员们发布一些关于 Hugging Face 相关的更新,包括我们的产品和平台更新、社区活动、学习资源和内容更新、开源库和模型更新等,我们将其称之为「Hugging News」,本期 Hugging News 有哪些有趣的消息,快来看看吧!

产品更新

Paper Pages 大更新

我们正在继续加深与预印本平台 (arXiv.org) 的合作和集成,除了可以在论文页面直接找到 Hugging Face 上的 Space 应用以外,我们又为 Hugging Face Hub 上线了一个论文页面,比如:
https://hf.co/papers/2211.05100

你还可以通过论文编号查询模型、数据集和 Space 应用,也可以在模型和数据集页面直接访问到论文页面:

与此同时,如果你是某个论文的作者之一,你还可以在 Hugging Face 的论文页面来“认领”和关联你的 Hugging Face 账号。

用户的个人页面也会显示自己的论文:

快来试试看吧!

Docker ❤️ HuggingFace

你可以在 Spaces 中使用 Docker SDK 来构建你的机器学习应用。上一周,我们还发布了一项与 Docker 的合作,现在你可以将你的 Space 应用一键使用 Docker 部署到其他环境中啦!

Hub 对 ipynb 文件 (Jupyter Notebook) 的两个重要更新

Hub 的页面现在可以直接渲染 .ipynb 文件 (Jupyter Notebook) 啦 ,除此之外,我们还与 Google Colab 团队合作,现在 Colab 可以直接打开托管在 Hugging Face Hub 上的 .ipynb 文件啦:

Zapier 平台加入对 Hugging Face 的集成支持

Zapier 是一个自动化工作平台,而无需编写任何代码即可快速自动化很多业务流程。通过与Hugging Face 的集成,你可以使用将任意其他工作流程与 Hugging Face 提供的 AI 模型能力结合。快来试试看吧:
https://zapier.com/apps/hugging-face/integrations

社区活动

Gradio 创意主题构建大赛

Gradio 团队发起了一个主题构建黑客松活动,快来参与吧!本次和黑客松活动为期两周,你可以发挥创意和脑洞任意构建有意思的 Gradio 主题。下面有一个视频来简单介绍如何上手制作一个主题:

https://www.bilibili.com/video/BV1Xv4y1H7it/

如果你想参与,请在这个页面查看参与方法:
https://hf.co/Gradio-Themes

也可以在这个 Space 应用里查看别人做的主题:
https://hf.co/spaces/gradio/theme-gallery

开源库更新

bloomz.cpp 让你在 Mac 和手机上运行 BLOOM 模型

bloomz.cpp 是一个用纯 C/C++ 实现 Hugging Face 的 BLOOM 系列模型推理的代码库。它建立在 @ggerganov 的 llama.cpp 库之上,支持使用
BloomForCausalLM.from_pretrained()
加载的所有模型。库还支持高级用法,可以进行更详细的设置和自定义操作。

GitHub 地址:
https://github.com/NouamaneTazi/bloomz.cpp

The Stack 数据集 v1.2 正式版发布

The Stack 是 BigCode 项目的一部分,这是一个包含了 300 种编程语言的开源代码数据集。
数据集页面:
https://hf.co/datasets/bigcode/the-stack

Common Voice 13 数据集现已「登陆」 Hugging Face

Common Voice 的多语言数据集是最大的公开语音数据集,Mozilla 相信,一组大型、可公开使用的语音数据集,将可促进基于机器学习的语音技术的创新,与健康的商业竞争。目前,Common Voice 13 数据集已经在 Hugging Face Hub 上发布,借助 Datasets 库,你只需要几行代码就可以使用它。
数据集页面:
https://hf.co/datasets/mozilla-foundation/common_voice_13_0

TRL v0.4.1 发布


TRL (Transformer Reinforcement Learning) 是一个用于训练具有增强学习的 Transformer 语言模型的库,可与

继续接前文
手撕商城系统架构设计与实现

本文主要讲解商城体系下产商品系统的设计。商城系统可以拆分成多个业务中台和多个应用服务。

1、产商品系统业务架构

产商品系统作为商城重要的基础信息组成部分,主要划分为产品信息和商品信息,产品信息保持最原始的产品基础属性和内容,商品信息则根据不同的售卖策略、营销价格属性或SKU进行组装而成。

因此商品源于产品而不同于产品。简单概括来说,商品是营销属性的产品。

2、产商品关键内容信息

产商品中心关键内容包括:产品信息、商品信息、目录管理、标签信息、产品字典库、产品分类、产品属性、价格版本管理、商品SKU组合。

产品信息应该包括产品基本信息,产品价格(与报价系统产品价格统一库),产品工艺术属性信息。

3、产商品系统边界

产商品系统与其他系统关系

订单系统与产商品系统调用关系

4、产商品结构模型设计

5、关键代码片断

@RestController
@RequestMapping("/customer-api/v1/collect")
public class GoodsCollectController {

    /**
     * 分页获取当前会员的收藏列表
     *
     * @return 收藏列表
     */
    @RestApi(module = "商品收藏-C端", name = "分页获取当前会员的收藏列表", logabled = true)
    @PostMapping("/page")
    public DataResponse<CustomizePage<GoodsCollectVO>> pageCurrentMemberGoodsCollect(@RequestBody @Valid GoodsCollectQry qry) {
        CustomizePage<MallGoodsCollectE> goodsCollectE = MallGoodsCollectE.queryInstance().pageCurrentMemberGoodsCollect(qry);
        return DataResponse.of(GoodsCollectVOConverter.convert(goodsCollectE));
    }

    /**
     * 加入收藏
     *
     * @param cmd 商品id
     * @return 收藏列表
     */
    @RestApi(module = "商品收藏-C端", name = "加入收藏", logabled = true)
    @PostMapping("/add")
    public DataResponse<Boolean> addGoodsCollect(@RequestBody @Valid AddGoodsCollectCmd cmd) {
        return DataResponse.of(MallGoodsCollectE.queryInstance().addGoodsCollect(cmd));
    }

    /**
     * 取消收藏
     *
     * @param id 收藏id
     * @return 操作结果
     */
    @RestApi(module = "商品收藏-C端", name = "取消收藏", logabled = true)
    @DeleteMapping("/{id}")
    public DataResponse<Boolean> deleteGoodsCollect(@PathVariable Long id) {
        return DataResponse.of(MallGoodsCollectE.queryInstance().deleteGoodsCollect(id));
    }

    /**
     * 根据 商品id 查询当前商品收藏情况
     *
     * @param cmd 商品id
     * @return 当前商品收藏情况
     */
    @RestApi(module = "商品收藏-C端", name = "根据 商品id 查询当前商品收藏情况", logabled = true)
    @PostMapping("/getGoodsCollect")
    public DataResponse<GetGoodsCollectVO> getGoodsCollect(@RequestBody GetGoodsCollectCmd cmd) {
        MallGoodsCollectE mallGoodsCollectE = MallGoodsCollectE.queryInstance().getGoodsCollect(cmd);
        return DataResponse.of(BeanToolkit.instance().copy(mallGoodsCollectE, GetGoodsCollectVO.class));
    }
}
@Slf4j
@Service
public class GoodsService {
    @Autowired
    private GoodsSkuRpcService goodsSkuRpcService;
    @Autowired
    private GoodsGateway goodsGateway;

    /**
     * 查询商品详情
     */
    public Map<String, SpuApiCO> mapSkuCO(List<String> skuIds) {
        if (CollUtil.isEmpty(skuIds)) {
            return Collections.emptyMap();
        }
        DataResponse<Map<String, SpuApiCO>> dataResponse = goodsSkuRpcService.mapByIds(skuIds);
        return ResponseUtil.resultValidate(dataResponse);
    }

    /**
     * 批量更新商品库存
     */
    public void updateInventory(List<UpdateInventoryDTO> dtoList) {
        goodsGateway.updateInventory(dtoList);
    }

    /**
     * 获取商品供应商集合
     */
    public Map<String, SupplierDTO> mapSupplierCO() {
        return goodsGateway.mapSupplierCO();
    }

    /**
     * 计算商品购买价格
     *
     * @param skuCO       商品信息
     * @param count       购买数量
     * @param memberLevel 会员等级
     * @param region      购买区域
     * @return 购买价格
     */
    public CalcPayPriceDTO calcPayPrice(SkuCO skuCO, Integer count, Integer memberLevel, String region) {
        //万
        BigDecimal tenThousand = BigDecimal.valueOf(10000);
        //该方法的中的价格单位为分
        //商品原价,原积分
        Long price = BigDecimalUtils.yuan2Penny(skuCO.getPrice());
        Long integral = skuCO.getIntegral();

        //需支付价格,积分,运费
        Long goodsTotalPrice = price;
        Long goodsTotalIntegral = integral;
        Long freight = 0L;

        // 1、计算会员等级差异化
        DiffPriceOption levelDifference = skuCO.getLevelDifference();
        if (levelDifference.enabled()) {
            DiffPriceTmpl.DiffPriceForLevel diffPriceForLevel = levelDifference.getTmpl().getDiffs().stream()
                    .filter(tmpl -> tmpl.getLevel().equals(memberLevel))
                    .findFirst()
                    .get();

            if (DiffPriceMode.PERCENTAGE_DISCOUNT.getValue().equals(levelDifference.getTmpl().getMode())) {
                // 1.1、结算比例调整
                Long percent = diffPriceForLevel.getPercent().multiply(BigDecimal.valueOf(100)).longValue();
                goodsTotalPrice = BigDecimal.valueOf(price * percent).divide(tenThousand, RoundingMode.HALF_UP).longValue();
                // 积分不足1取1
                BigDecimal integralDecimal = BigDecimal.valueOf(integral * percent);
                goodsTotalIntegral = integralDecimal.compareTo(tenThousand) > 0 ?
                        integralDecimal.divide(tenThousand, RoundingMode.HALF_UP).longValue()
                        : integralDecimal.divide(tenThousand, RoundingMode.UP).longValue();
            } else if (DiffPriceMode.EXTRA_PAYMENT.getValue().equals(levelDifference.getTmpl().getMode())) {
                // 1.2、需额外支付
                if (diffPriceForLevel.getExtraPrice() != null) {
                    Long extraPrice = BigDecimalUtils.yuan2Penny(diffPriceForLevel.getExtraPrice());
                    goodsTotalPrice = (price + extraPrice);
                }
                if (diffPriceForLevel.getExtraIntegral() != null) {
                    goodsTotalIntegral = (integral + diffPriceForLevel.getExtraIntegral());
                }
            } else {
                throw new ServiceException("价格结算失败");
            }
        }
        // 购物车结算时,收货地址还没选,选了再计算
        if (StringUtil.isNotEmpty(region)) {
            // 2、计算运费
            ShippingCostOption freeShippingRange = skuCO.getFreeShippingRange();
            if (freeShippingRange.enabled()) {
                UCRegionCacheCO customerRegion = MtdsBaseUCRegionCacheUtils.getUCRegionCacheCOById(region);
                Optional<ShippingCostTmpl.RegionalCost> regionalCostOptional = freeShippingRange.getTmpl().getRegionalCosts().stream()
                        .filter(tmpl -> customerRegion.getPids().contains(tmpl.getRegionId()))
                        .findFirst();

                if (regionalCostOptional.isPresent()) {
                    ShippingCostTmpl.RegionalCost regionalCost = regionalCostOptional.get();
                    // 2.1 满足包邮条件
                    if (regionalCost.getFreeEnabled() == 1 && count >= regionalCost.getFreeQty()) {
                        freight = 0L;
                    } else {
                        // 2.2 计算运费
                        if (count <= regionalCost.getBaseQty()) {
                            freight = freight + BigDecimalUtils.yuan2Penny(regionalCost.getBasePrice());
                        } else {
                            freight = freight + BigDecimalUtils.yuan2Penny(regionalCost.getBasePrice());

                            int increaseCount = (count - regionalCost.getBaseQty());
                            long extraFreight = BigDecimalUtils.yuan2Penny(regionalCost.getIncreasePrice())
                                    * increaseCount;
                            freight = freight + (extraFreight);
                        }
                    }
                }
            }
        }

        //支付金额
        Long payPrice = (goodsTotalPrice * count) + freight;
        return CalcPayPriceDTO.builder()
                .skuId(Long.valueOf(skuCO.getId()))
                .oldGoodsTotalPrice(price * count)
                .goodsTotalPrice(goodsTotalPrice * count)
                .payPrice(payPrice)
                .freight(freight)
                .oldGoodsTotalIntegral(integral * count)
                .goodsTotalIntegral(goodsTotalIntegral * count)
                .build();
    }
}
@Slf4j
@Component
public class GoodsGatewayImpl implements GoodsGateway {
    @Autowired
    private GoodsSkuRpcService goodsSkuRpcService;
    @Autowired
    private GoodsSupplierRpcService supplierRpcService;

    @Override
    public void updateInventory(List<UpdateInventoryDTO> dtoList) {
        List<SkuIncrementCmd> skuIncrementCmds = BeanToolkit.instance().copyList(dtoList, SkuIncrementCmd.class);
        Response response = goodsSkuRpcService.increment(skuIncrementCmds);
        if (!response.getStatus()) {
            throw new RpcErrorException(response.getMessage(), "商品");
        }
    }

    @Override
    public Map<String, SupplierDTO> mapSupplierCO() {
        DataResponse<List<SupplierCO>> response = supplierRpcService.listAll();
        List<SupplierCO> supplierCOS = ResponseUtil.resultValidate(response);
        if (CollUtil.isEmpty(supplierCOS)) {
            return Collections.emptyMap();
        }

        List<SupplierDTO> supplierDTOS = BeanToolkit.instance().copyList(supplierCOS, SupplierDTO.class);
        return supplierDTOS.stream().collect(Collectors.toMap(SupplierDTO::getId, Function.identity()));
    }
}

原文链接:
Git Commit Message 应该怎么写?

最近被同事吐槽了,说我代码提交说明写的太差。其实都不用他吐槽,我自己心里也非常清楚。毕竟很多时候犯懒,都是直接一个
-m "fix"
就提交上去了。

这样做是非常不好的,我也是自食恶果,深受其害。特别是查看历史提交记录时,想通过提交说明来了解当时的功能变更,基本不可能,都得点进去看代码才行。

所以这两天看了一些
如何写好提交说明
的资料,系统地学习了一下。虽然团队没有这方面的要求,但是想要进步,得对自己提更高的要求才行。

一般使用 git 提交代码的话,可以使用
-m
参数来指定提交说明,比如:

$ git commit -m "hello world"

如果一行不够,可以只执行
git commit
,这样就会跳出文本编辑器来写多行:

$ git commit

Commit Message 格式

Commit Message 包括三个部分:Header,Body 和 Footer。

<Header>

<Body>

<Footer>

其中,Header 是必需的,Body 和 Footer 可以省略。

Header 部分只有一行,包括三个字段:type(必需)、scope(可选)、subject(必需)。

<type>(<scope>): <subject>

type

type 用于说明 commit 的类别,具体的标识如下:

  • feat
    :一个新的功能(feature);
  • fix
    :修复 bug;
  • docs
    :修改文档,比如 README.md、CHANGELOG.md 等;
  • style
    :修改代码的格式,不影响代码运行的变动,比如空格、格式化代码、补齐句末分号等等;
  • refactor
    :代码重构,没有新功能的添加以及 bug 修复的代码改动;
  • perf
    :优化代码以提高性能;
  • test
    :增加测试或优化改善现有的测试;
  • build
    :修改影响项目构建文件或外部依赖项,比如 npm、gulp、webpack、broccoli 等;
  • ci
    :修改 CI 配置文件和脚本;
  • chore
    :其他非 src 路径文件和测试文件的修改,比如 .gitignore、.editorconfig 等;
  • revert
    :代码回退;

scope

scope 用于说明 commit 的影响范围,比如数据层、控制层、视图层等等,视项目不同而不同。

如果你的修改影响了不止一个 scope,就可以使用
*
代替。

subject

subject 是 commit 目的的简单描述,不超过 50 个字符,结尾不需要句号。

Body

Body 部分是对本次 commit 的详细描述,可以分多行。

Body 部分应该说明代码变动的动机,以及与以前行为的对比。

More detailed explanatory text, if necessary.  Wrap it to
about 72 characters or so.

Further paragraphs come after blank lines.

- Bullet points are okay, too
- Use a hanging indent

Footer 部分主要用于两种情况:不兼容变动和处理 Issue。

不兼容变动

如果当前代码与上一个版本不兼容,则 Footer 部分以
BREAKING CHANGE:
开头,后面就是对变动的描述、以及变动理由和迁移方法。

BREAKING CHANGE: Previously, $compileProvider.preAssignBindingsEnabled was set to true by default. This means bindings were pre-assigned in component constructors. In Angular 1.5+ the place to put the initialization logic relying on bindings being present is the controller $onInit method.

To migrate follow the example below:

Before:

​```js
angular.module('myApp', [])
    .component('myComponent', {
        bindings: {value: '<'},
        controller: function() {
        this.doubleValue = this.value * 2;
    }
});
​```

After:
​```js
angular.module('myApp', [])
    .component('myComponent', {
        bindings: {value: '<'},
        controller: function() {
            this.$onInit = function() {
                this.doubleValue = this.value * 2;
            };
        };
        this.doubleValue = this.value * 2;
    }
});
​```

Don't do this if you're writing a library, though, as you shouldn't change global configuration then.

处理 Issue

处理 Issue 分为两种情况,分别是关联 Issue 和关闭 Issue。

比如本次提交如果和某个 issue 有关系:

Issue #1, #2, #3

如果当前提交信息解决了某个 issue:

Close #1, #2, #3

Revert

还有一种特殊情况,如果当前 commit 用于撤销以前的 commit,则必须以
revert:
开头,后面跟着被撤销 commit 的 Header。

revert: feat(pencil): add 'graphiteWidth' option

This reverts commit 667ecc1654a317a13331b17617d973392f415f02.

Body 部分的格式是固定的,必须写成
This reverts commit &lt;hash>.
,其中 hash 是被撤销 commit 的 SHA 标识符。

如果当前 commit 与被撤销的 commit,在同一个发布(release)里面,那么它们都不会出现在 Change log 里面。如果两者在不同的发布,那么当前 commit,会出现在 Change log 的 Reverts 小标题下面。

最后来看一个例子,算是一个总结,至于具体内容还是要根据实际情况来填写。

feat: 添加了分享功能

给每篇博文添加了分享功能

- 添加分享到微博功能
- 添加分享到微信功能
- 添加分享到朋友圈功能

Issue #1, #2
Close #1

插件推荐

有了这些规范,也知道怎么写了,但是不是会担心记不住呢?不要怕,有插件可以用,如果使用 VsCode 的话,可以安装一个叫
Commit Message Editor
的插件。

可以根据提示信息直接写:

也可以使用表单的方式,有选项可以选择:

这样不仅可以很方便地写提交说明了,还可以使提交说明更加的规范。

以上就是本文的全部内容,如果觉得还不错的话欢迎
点赞

转发

关注
,感谢支持。


参考文章:

推荐阅读: