在 DDD 是银弹吗? 中,D瓜哥分享了关于领域驱动设计的三个问题。最近在读一本书 《架构设计2.0:大型分布式系统架构方法论与实践》。(这本书还不错,推荐)这本书中,花了两个章节的篇幅,重点谈论了领域驱动设计。引用书中的观点,结合个人开发经验,再来谈一谈 DDD 是否是银弹?
软件建模的困难 首先,必须面对的一个事实是:软件建模,困难重重;尤其是对于复杂业务的建模,更是难上加难。
对于复杂业务的软件开发,其生命周期大概分为如下五个阶段:
确定业务目标和业务价值。
比如某消费信贷业务。
目标被拆解成一系列核心功能点。
比如消费信贷下的授信、交易、账务等。
围绕这些功能点定义业务流程、业务规则,以及整个过程设计什么样的业务数据或业务对象。
比如账单分期金额必须大于 100 元。
领域建模。
比如对账务系统进行建模。
基于领域模型做技术架构的设计。
比如是否要做读写分离?是否要做分库分表等?
软件建模的本质是找出现实世界中的“不变形”。但是,现实世界中,唯一不变的就是这个世界在不断变化!所以,建模的过程也是一个反复的过程。如下图:
图 1. 复杂业务软件开发的生命周期 几乎不存在稳定的领域模型 我们追求一个稳定的领域模型,但是,现实却给了我们重重一击:稳定的模型几乎不可能做到。原因如下:
意识问题。
在消费、业务及产品等关注的是业务流程。唯独开发人员要将业务流程转化成业务模型。
现实世界的复杂性。
现实业务是复杂的,建模只是抽取了一个现实业务某一时刻的业务形态。但是,业务形态会有变化的,比如取现前期不可分期,后期业务迭代可能就会运行进行分期。
迭代速度。
互联网公司要求“小步快跑,快速迭代”。这与模型的稳定其实是矛盾的。为了业务的迭代速度,只能牺牲模型的稳定性,为了赶工期,只能在模型上不断打补丁。
火候的掌握。
开发人员的设计能力无法一蹴而就。既需要思考,又需要反复练习。在快速的业务迭代和人员流动下,开发人员根本没有时间锤炼自己的设计能力。那么,对于设计火候的掌握,也就很难达到理想中的水平。
领域驱动设计的困难 书中总结了实施领域驱动设计的五个困难,D瓜哥逐一谈谈自己的看法:
领域驱动设计本身只是一套思维方法,而不是要严格执行的标准,所以其本身弹性很大。
这个问题,D瓜哥在上一篇文章中已经讨论过了。弹性太大,就有太多值得商榷的地方,也许初次开发,还可以按照某个人的想法一以贯之。但是,随着参与维护的人员增多,每个人都会不由自主地会带入个人的一些想法,各种想法的碰撞,必然就会引入代码结构的混乱。
思维方式的转换很难。
绝大多数面向业务的开发人员,尤其是 Java 开发人员,对三层架构已经有根深蒂固的认识。思维方式已经被打上深深的烙印,想要改变,坦白讲,极其困难。尤其是,没有一个同一个的标准和广泛认可的实现范例,完全靠摸着石头过河,必然会“一千个读者,就有一千个哈姆雷特。”
领域驱动设计的实施需要强大的技术基础实施来保证。
D瓜哥私以为这个倒不是什么问题。针对技术问题,尤其是一些共性问题,都有成熟的解决方案。只要能合理搭积木,就可以解决相应的问题。
大量存量的老系统,重构成本大于收益,没有重构动力。
编程第一准则:代码能跑就不要动。重构引入的问题谁来解决?重构带来的事故谁来负责?这个时候必须祭出这张图了:
图 2. 代码能跑就不要动 当然,私以为不是程序员反感重构代码,更多是因为下面这个因素。
在互联网的快速开发迭代面前,很少有人可以静下心来在软件方法论层面去精雕细琢,更多的是快速堆砌功能,完成业务需求开发。
业务的快速迭代,导致根本没有时间让开发人员去优化代码。可口的饭菜需要恰当的火候和足够的时间,优雅的软件建模也需要恰当的火候和足够的时间。精心地软件建模需要三个月,业务让你一个月上线,而且还是加班加点才能干完。结合实际来看,绝大多数情况都会想业务妥协吧?!
领域驱动设计的出路 书中的观点是做个折中:在宏观层面,遵循领域驱动设计的方法论;在微观层面,不严格遵循领域驱动设计的方法论。
D瓜哥是这样理解的:可以利用领域驱动设计里面的限界上下文的思想,把领域做个分割,划分成业务更聚合的子域。在子域内部,提炼出统一语言,来规范业务、产品和开发沟通的业务术语。在子域交互的接口层面,进行精心设计,精雕细琢。至于子域及接口的内部实现,就交给开发团队自己决策,只要满足对应的技术指标(比如每秒要支撑多大的访问量)即可。
在部门内部讨论时,D瓜哥还给出了一个更具操作性和落地性的方案:现实面临的问题是代码冗余,技术欠债,不容易维护。先放下关于领域驱动设计的无谓讨论,利用每一次开发的机会,把冗余代码删除,把代码重构和优化,一步一步地精炼代码,即使不谈领域驱动设计,相信在逐步重构和优化下,技术欠债会逐渐弥补,可维护性也会逐步提高。
史前时期最骇人的景象,莫过于一群巨兽在焦油坑里做垂死前的挣扎。不妨闭上眼睛想像一下,你看到了一群恐龙、长毛象、剑齿虎正在奋力挣脱焦油的束缚,但越挣扎,焦油就缠得越紧,就算他再强壮、再厉害,最后,都难逃灭顶的命运。过去十年间,大型系统的软件开发工作就像是掉进了焦油坑里……
— 佛瑞德·布鲁克斯(Frederick P. Brooks) 《人月神话》 应该早在 2019 年,在 左耳朵耗子哥 的推荐下阅读了 《领域驱动设计》,并将读书摘要整理成几篇文章:
《领域驱动设计》读书笔记(一):运用领域模型
《领域驱动设计》读书笔记(二):模型驱动设计的构造块
《领域驱动设计》读书笔记(三):通过重构来加深理解
《领域驱动设计》读书笔记(四):战略设计
部门要搞 DDD 和体系化建设,正好有一个核心项目要做重构,领导让实践一下领域驱动设计,苦于没有范例可以参考,感觉无处下手,所以又读了 《中台架构与实现·基于DDD和微服务》(最早读的是极客时间专栏,后专栏编撰成该书)。
后来,又陆陆续续看了好多领域驱动设计的相关文章。对于领域驱动设计,即了解过,也实践过。所以,结合自身的经历和体会,谈一谈我的感受。不吹不黑,重点谈三个问题。
1. 如何快速上手? 上面介绍了一下D瓜哥的个人经历,是付出了一点的时间和精力的,由此引出了第一个问题:如何快速上手?对于一个工作多年,经验丰富,也算勤奋好学的高级码农,上手还如此困难重重,那么对于一个刚刚参加工作的职场新人,上手是否会更加困难?又该如何克服这个困难?
任何一家公司,尤其是大型技术公司,都是由初中高级工程师组成的,而且成员人数也是由多到少,参与实际开发工作,大概率也会由多到少,初级开发工程师干了大量的实际编码工作。如果无法吸引大多数的初级工程师参与进来,只有个别的高级工程师去落地,那么,所谓的领域驱动设计,只能成为空中楼阁,海市蜃楼。华而不实,无法落地。
但是,由于经验少,这对于初级工程师来说,也许是一个优势。毕竟,一张白纸,可以画出各种美丽的画卷。中高级工程师已经习惯于传统的开发模式,思维已经定格。但是,初级工程师,反倒是嗷嗷待哺,更容易塑性。可惜的是,现在没有好的示例可以学习。
2. 哪里有可以参考的示例? 快速上手的最好办法,就是给一个完整的示例,拿来直接抄作业。对于入门的程序员,学东西上手最快的办法就是抄代码。把示例代码,拿过来改吧改吧就能跑起来,无形中就学会怎么写代码了。对于传统的三层架构,有太多的示例可以来学习了,比如 SpringSide。
从 《Domain-Driven Design》 这本书在 2003 年出版到现在,已经有 21 年了。到现在为止,也没有见到一个开源的、能运行起来的基于领域驱动设计的项目。也可能是鄙人孤陋寡闻,坐井观天,没有发现。如果谁发现了,欢迎向我反馈。
作为对比,我们来看一下 Spring 的发展过程。Spring 的思想最早是在 《J2EE Development without EJB》 这本书里出现的,这本书是在 2004 年 6 月出版的。这本书出版后,开源社区根据这本书里面的思想及代码片段,开发出了 Spring 框架。在两年后,Spring 之父 Rod Johnson 接着出版了 《Professional Java Development with the Spring Framework》,系统介绍了一下 Spring 框架的各种使用案例。到 2008 年我上大学的时候,在国内的培训行业,已经开始重点讲解 Spring 了。
其实,D瓜哥想拿传统的三层架构的发展来做对比,可惜没有找到更确切的时间线。 期待一个完整的、基于领域驱动设计的、能正常运行起来的开源项目尽早出现!
D瓜哥在 Spring 扩展点概览及实践 中概要性地介绍了一下 Spring 的核心扩展点。里面也提到了 BeanFactoryPostProcessor 和 BeanDefinitionRegistryPostProcessor,但仅仅提了一句,没有深入研究。在 Spring 扩展点实践:整合 MyBATIS 中,由于 MapperScannerConfigurer 实现了 BeanDefinitionRegistryPostProcessor 接口,也只是简单介绍了一些作用,又一次没有深入研究。
最近,在开发一个插件时,遇到了一个问题:利用 BeanFactoryPostProcessor 对一些特定 BeanDefinition 设置属性,但生成的 Bean 却没有相关的属性值。由此,对 BeanFactoryPostProcessor 做了一些研究。记录一下,以备不时之需。
Spring 启动流程简介 在 Spring 启动流程概述 中,D瓜哥对 Spring 的启动流程做了比较详细的介绍。同时画了一张启动流程图,如下:
图 1. AbstractApplicationContext.refresh — 重塑容器 从该图中可以明显看到,如果需要对 Spring 的 BeanDefinition 做些修改,那么,就需要通过实现 BeanFactoryPostProcessor 接口,来对 Spring 做些扩展。坦白讲,为了上述流程图只展示了一个非常概要性的流程。如果深入一下 invokeBeanFactoryPostProcessors 方法的细节,会发现这又是一番天地。
BeanFactoryPostProcessor 调用详解 D瓜哥把 invokeBeanFactoryPostProcessors 方法的流程图也画了出来,细节如下:
图 2. BeanDefinitionRegistryPostProcessor & BeanFactoryPostProcessor 调用过程 从这张流程图上可以看出 BeanFactoryPostProcessor 的调用过程,比在 Spring 启动流程概述 中介绍的要复杂很多:
首先,执行 BeanDefinitionRegistryPostProcessor#postProcessBeanDefinitionRegistry 方法,顺序如下:
关于 BeanDefinitionRegistryPostProcessor 的处理流程,D瓜哥在 Spring 扩展点概览及实践:BeanDefinitionRegistryPostProcessor 中有更详细的描述,不了解的朋友请参考那篇文章的介绍。
在 OpenJDK 21 升级指南 中提到, OpenRewrite 可以帮忙解决一些升级 OpenJDK 中发现的问题。随着不断的探索,D瓜哥发现,OpenRewrite 的功能远远不止这些。下面就挑选一些重要的功能来给大家做一些讲解。
为了方便查看改动点,建议将代码交给版本管理工具,比如 Git,来管理。 快速入门 OpenRewrite 是一套对源码做重构的大型生态系统,可以帮助开发人员减少技术债。所以,它提供了一套的相关工具。对于大多数开发人员来说,最方便的也许就是基于 Maven 插件的相关工具。这里以对 Java 的 import 语句排序来为示例展示一下 OpenRewrite 的使用方法。
在项目的 pom.xml 中增加如下配置:
<!-- @author: D瓜哥 · https://www.diguage.com --> <plugin> <groupId>org.openrewrite.maven</groupId> <artifactId>rewrite-maven-plugin</artifactId> <version>5.30.0</version> <configuration> <activeRecipes> <!-- import 排序 --> <!-- https://docs.openrewrite.org/recipes/java/orderimports --> <recipe>org.openrewrite.java.OrderImports</recipe> </activeRecipes> </configuration> </plugin> 然后执行如下命令:
mvn rewrite:run 执行会输出一大堆东西,这里就不再展示,执行完成后,使用 Git 查看一下改动点。如下图:
图 1. 使用 OpenRewrite 排序 import 的改动点 将这些修改点提交,就完成了一次优化, OpenRewrite 的基本使用,你学废了吗?
这里再多说一句: 由于 OpenRewrite 精巧的设计,可以通过使用不同的处方,进行各种各样的优化。所以,最重要的一点就是了解 OpenRewrite 各种不同的处方及使用办法。下面就介绍一下常用的处方及使用办法。
常用处方 升级到 Java 21 在 OpenJDK 21 升级指南 中提到,可以使用“科技与狠活”来解决很多升级中遇到的问题。这里就来实操一把。
问题 什么是大页(Large Page)?什么是透明大页(Transparent Huge Page)?它对我有什么帮助?
理论 虚拟内存现在已被视为理所当然。现在只有少数人还记得,更不用说做一些“真实模式”编程了,在这种情况下,你会接触到实际的物理内存。相反,每个进程都有自己的虚拟内存空间,该空间被映射到实际内存上。例如,两个进程在相同的虚拟地址 0x42424242 上拥有不同的数据,而这些数据将由不同的物理内存支持。现在,当程序访问该地址时,应将虚拟地址转换为物理地址。
图 1. 虚拟内存地址与物理内存地址之间的关系 这通常由操作系统维护 “页表”,硬件通过“页表遍历”来实现地址转换。如果在页面粒度上维护翻译,整个过程就会变得简单。但这样做的成本并不低,而且每次内存访问都需要这样做!因此,还需要对最新的翻译进行小型缓存,即 转译后备缓冲区(Translation Lookaside Buffer (TLB))。TLB 通常很小,只有不到 100 个条目,因为它的速度至少要与 L1 缓存相当,甚至更快。对于许多工作负载来说,TLB 未命中和相关的页表遍历需要大量时间。
既然我们无法将 TLB 做得更大,那么我们可以做其他事情:制作更大的页面!大多数硬件有 4K 基本页和 2M/4M/1G “大页”。用更大的页来覆盖相同的区域,还能使页表本身更小,从而降低页表遍历的成本。
在 Linux 世界中,至少有两种不同的方法可以在应用程序中实现这一点:
hugetlbfs。切出系统内存的一部分,将其作为虚拟文件系统公开,让应用程序通过 mmap(2) 从其中获取。这是一个特殊的接口,需要操作系统配置和应用程序更改才能使用。这也是一种“要么全有,要么全无”的交易:分配给 hugetlbfs(持久部分)的空间不能被普通进程使用。
透明大页(Transparent Huge Pages (THP))。让应用程序像往常一样分配内存,但尽量以透明方式为应用程序提供大容量页面支持的存储空间。理想情况下,不需要更改应用程序,但我们会看到应用程序如何从了解 THP 的可用性中获益。但在实际应用中,会产生内存开销(因为会为小文件分配整个大页面)或时间开销(因为 THP 有时需要对内存进行碎片整理以分配页面)。好在有一个中间方案:通过 madvise(2) 可以让应用程序告诉 Linux 在哪里使用 THP。
不明白为什么术语中会交替使用 "large "和 "huge"。总之,OpenJDK 支持这两种模式:
$ java -XX:+PrintFlagsFinal 2>&1 | grep Huge bool UseHugeTLBFS = false {product} {default} bool UseTransparentHugePages = false {product} {default} $ java -XX:+PrintFlagsFinal 2>&1 | grep LargePage bool UseLargePages = false {pd product} {default}
“JVM 剖析花园”是由 JVM 研发专家及性能极客 Aleksey Shipilëv 撰写的一个系列文章,专门介绍一些有关 JVM 的基本知识。笔者也是前几年无意间发现的一片宝藏文章。早就有翻译过来,介绍给大家的想法,可惜一直未能付诸实践。最近在查资料时,无意间又翻到了这个系列,遂下定决心,完成这个萌发已久的小想法。
为了便于理解,对该系列的名字做了微调,原文是“JVM Anatomy Quarks”,将原文的“Quarks”(夸克)翻译为了“花园”。
“JVM 解剖花园”是一个正在进行中的小型系列文章,每篇文章都会介绍一些有关 JVM 的基本知识。这个名字强调了一个事实,即单篇文章不能孤立地看待,这里描述的大部分内容都会很容易地相互影响。
阅读这篇文章大约需要 5-10 分钟。因此,它只针对单一主题、单一测试、单一基准和单一观察进行深入探讨。这里的证据和讨论可能是轶事,并没有对错误、一致性、写作风格、语法和语义错误、重复或一致性进行实际审查。请自行承担使用和/或信任的风险。
以上是该系列介绍。这里介绍一次,后续文章不再赘述。 问题 众所周知,Hotspot 可以进行 锁粗化优化,有效合并多个相邻的锁定块,从而减少锁定开销。它能有效地对如下代码做优化:
synchronized (obj) { // statements 1 } synchronized (obj) { // statements 2 } 优化后:
synchronized (obj) { // statements 1 // statements 2 } 现在,今天提出的一个有趣问题是:Hotspot 是否会对循环进行这种优化?例如:
for (...) { synchronized (obj) { // something } } 是否会被优化成如下这样:
synchronized (this) { for (...) { // something } } 理论上,没有什么能阻止我们这样做。我们甚至可以把这种优化看作是 循环判断外提,只不过这里是针对锁而已。然而,这样做的缺点是有可能使锁变得过于粗糙,从而导致特定线程在执行大循环时占用锁。
实验 要回答这个问题,最简单的方法就是找到当前 Hotspot 优化的正面证据。幸运的是,有了 JMH,这一切都变得非常简单。它不仅有助于建立基准,还有助于工程中最重要的部分—基准分析。让我们从一个简单的基准检查程序开始:
@Fork(..., jvmArgsPrepend = {"-XX:-UseBiasedLocking"}) @State(Scope.Benchmark) public class LockRoach { (1) int x; @Benchmark @CompilerControl(CompilerControl.Mode.DONT_INLINE) public void test() { for (int c = 0; c < 1000; c++) { synchronized (this) { x += 0x42; } } } } 1 完整代码在 这里。 这里有几个重要的技巧:
使用 -XX:-UseBiasedLocking 禁用偏向锁可以避免更长的预热时间,因为偏向锁不会立即启动,而是会在初始化阶段等待 5 秒(参见 BiasedLockingStartupDelay 选项)。
禁用 @Benchmark 的方法内联有助于在反汇编时将其分离。
增加一个神奇的数字 0x42,有助于在反汇编中快速找到增量。
公司最近一年在推进降本增效,在用尽各种手段之后,发现应用太多,每个应用都做跨机房容灾部署,则最少需要 4 台机器(称为容器更合适)。那么,将相近应用做一个合并,减少维护项目,提高机器利用率就是一个可选方案。
经过前后三次不同的折腾,最后探索出来一个可行方案。记录一下,分享出来,希望对有相关需求的研发童鞋有所帮助。下面按照四种可能的方案,分别做介绍。另外,为了方便做演示,专门整了两个演示项目:
diguage/merge-demo-boot — 合并项目,下面简称为 boot。
diguage/merge-demo-web — 被合并项目,下面简称为 web。
Jar 包引用 这个方式,可能是给人印象最容易的方式。仔细思考一下,从维护性的角度来看,这个方式反而是最麻烦的方式,理由如下:
web 项目每次更新,都需要重新打包发布新版; boot 项目也需要跟着更新发布。拉一次屎,脱两次裤子。属实麻烦。
还需要考虑 web 项目的加载问题,类似下面要描述的,是否共用容器:
共用容器 — 这是最容器想到的方式。但是这种方式,需要解决 Bean 冲突的问题。
不共用容器 — 这种方式需要处理 web 容器如何加载的问题。默认应该是无法识别。
基于这些考虑,这种方式直接被抛弃了。
仓库合并,公用一套容器 这是第一次尝试使用的方案。也是遇到问题最多的方案。
将两个仓库做合并。
将 web 仓库的地址配置到 boot 项目里: git remote add web git@github.com:diguage/merge-demo-web.git;
在 boot 项目里,切出来一个分支: git switch -c web;
将 web 分支的提交清空: git update-ref -d HEAD,然后做一次提交;
将 web 项目的代码克隆到 web 分支上: git pull --rebase --allow-unrelated-histories web master;注意,这里需要加 --allow-unrelated-histories 参数,以允许不相干的仓库进行合并。
从 boot 项目的 master 分支上,切出来一个合并分支: git switch -c merge;
将 web 项目向 boot 项目合并: git merge --allow-unrelated-histories web;注意,这里需要加 --allow-unrelated-histories 参数,以允许不相干的仓库进行合并。
处理代码冲突,完成合并即可。
最近有小伙伴在开发时,遇到了一个 Spring 占位符,例如 ${token}, 在不同环境下处理不一致的问题,正好对 Spring 对占位符的处理也有一些不清楚的地方,趁此机会,把 Spring 对占位符的处理机制深入了解一下,方便后续排查问题。
经常阅读D瓜哥博客的朋友可能知道,D瓜哥在 Spring 扩展点实践:整合 Apache Dubbo(一): Spring 插件机制简介 中已经介绍了 Spring 的插件机制。在阅读以下内容之前,建议大家先去阅读一下这篇文章中“Spring 插件机制简介”章节的内容,以便于无缝衔接。
在分析的过程中发现, Spring 对占位符有两种截然不同的出来阶段:① XML 配置文件中的占位符;② Java 源代码中 @Value 注解中的占位符。由于内容较多,一篇讲解完有些过长,所以分三篇文章来分别介绍这两种处理过程。
本篇首先来介绍一下对 XML 配置文件中的占位符的处理。
示例代码 在正式开始之前,先来看一下示例代码:
UserRpc.java /** * @author D瓜哥 · https://www.diguage.com * @since 2023-05-02 10:23:49 */ public class UserRpc { @Value("${user.appId}") private String appId; // 这里不使用注解,而是使用 XML 配置 // @Value("${user.token}") private String token; } token.properties user.appId=dummyAppId user.token=dummyToken spring.xml <?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd"> <!-- @author D瓜哥 · https://www.diguage.com --> <context:annotation-config/> <bean id="userRpc" class="com.diguage.truman.context.UserRpc"> <!-- XML 配置的占位符实例在此 --> <property name="token" value="${user.token}"/> </bean> <context:property-placeholder location="classpath:token.properties"/> </beans> <bean> 标签处理 在 Spring 启动流程概述 中,已经介绍过,Spring 的启动过程几乎都被封装在 AbstractApplicationContext#refresh 方法中。在 refresh 方法中调用了 refreshBeanFactory 方法;在 refreshBeanFactory 方法执行过程中,调用了 loadBeanDefinitions 方法。而 BeanDefinition 的加载是由 org.springframework.context.support.AbstractRefreshableApplicationContext#loadBeanDefinitions 来完成的。通过 XML 文件配置的 Bean 是由 org.springframework.context.support.AbstractXmlApplicationContext#loadBeanDefinitions(org.springframework.beans.factory.support.DefaultListableBeanFactory) (AbstractRefreshableApplicationContext 的子类)处理完成的。处理过程的时序图如下:
文章还没写完,提前放出防止出现 404。稍后慢慢更新,敬请期待: 细说编码与字符集 - "地瓜哥"博客网
文章还没写完,提前放出防止出现 404。稍后慢慢更新,敬请期待: 细说编码与字符集 - "地瓜哥"博客网
文章还没写完,提前放出防止出现 404。稍后慢慢更新,敬请期待: 细说编码与字符集 - "地瓜哥"博客网
前段时间要研究 Hessian 编码格式,为了搞清楚 Hessian 对字符串的编码,就顺路查了好多编码和字符集的工作,理清了很多以前模糊的知识点。下面整理一下笔记,也梳理一下自己的思路和理解。
ASCII 码 计算机起源于美国,他们对英语字符与二进制位之间的对应关系做了统一规定,并制定了一套字符编码规则,这套编码规则被称为 American Standard Code for Information Interchange,简称为 ASCII 编码
其实,ASCII 最早起源于电报码。最早的商业应用是贝尔公司的七位电传打字机。后来于 1963 年发布了该标准的第一版。在网络交换中使用的 ASCII 格式是在 1969 年发布的,该格式在 2015 年发展成为互联网标准。点击 RFC 20: ASCII format for network interchange,感受一下 1969 年的古香古色。
ASCII 编码一共定义了128个字符的编码规则,用七位二进制表示(0x00 - 0x7F), 这些字符组成的集合就叫做 ASCII 字符集。完整列表如下:
ASCII 码可以说是现在所有编码的鼻祖。
编码乱战及 Unicode 应运而生 ASCII 编码是为专门英语指定的编码标准,但是却不能编码英语外来词。比如 résumé,其中 é 就不在 ASCII 编码范围内。
随着计算机的发展,各个国家或地区,甚至不同公司都推出了不同的编码标准,比如中国推出了 GB2312、GBK 以及 GB18030;微软推出了 Windows character sets 。
在 Java 虚拟机操作码探秘:常量指令 中对 Java 虚拟机操作码中关于常量操作的指令(操作码)做了初步介绍。估计会有人疑问:文中的“栈”、“栈顶”等是什么?接下来就准备解答这些疑问。
在答疑解惑之前,先来了解一下 Java 编译器对 Java 代码中的代码块是如何处理的?常见的代码块有普通代码块和静态代码块,下面对其做分别介绍。由于涉及到构造函数,所以,先对构造函数做一个介绍。
构造函数 无构造函数 先来看看当没有声明构造函数时,编译结果是什么样的:
/** * 无构造函数示例 * * @author D瓜哥 · https://www.diguage.com */ public class Example { } 编译后,使用 javap -c 查看一下编译结果:
$ javap -c Example Compiled from "Example.java" public class Example { public Example(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return } 从结果上来看:编译器自动给没有声明构造函数的类,生成了一个无参构造函数,并且在其中调用了父类(这里是 Object)的无参构造函数。这是大家都熟知的基础知识。
有参构造函数 再来看看当有声明参数的构造函数时,编译结果是什么样的:
/** * 有参构造函数示例 * * @author D瓜哥 · https://www.diguage.com */ public class Example { public Example(int i) { } } 编译后,使用 javap -c 查看一下编译结果:
$ javap -c Example Compiled from "Example.java" public class Example { public Example(int); (1) Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return }