
大道至简! https://waylau.com/
近期,在HarmonyOS官网发布《鸿蒙生态应用开发白皮书》V1.0版本(以下简称《白皮书》)。笔者通读了《鸿蒙生态应用开发白皮书》,总结了读后感。如果熟悉鸿蒙开发,或者熟悉鸿蒙的文档,那么对于《白皮书》的内容应该就不会陌生。《白皮书》主要是从鸿蒙生态应用的背景、核心技术理念、开发能力平台、开发与测试、上架与分发、自由流转与分布式运行环境、运维分析、案例参考等八个方面进行阐述。背景鸿蒙生态应用的背景介绍的是鸿蒙生态应用的产生的背景、面临的机遇和挑战,以及未来的发展趋势。对于开发者而言,熟读这个背景是至关重要的,因为需要你从这个背景判断出你是否需要加入这个鸿蒙生态,未来是否看好这个鸿蒙生态,是否值得投入这个生态的开发。核心技术理念核心技术理念分为三部分来介绍,这其实是就是鸿蒙生态的最为核心的部分了,也是最能吸引开发者的部分:一次开发,多端部署可分可合,自由流转统一生态,原生智能开发能力平台开发能力平台主要是华为公司为了让更多开发者能加容易的进行鸿蒙生态应用的开发,所提供的平台支持。从笔者亲身经历来看,开发能力平台目前已经是非常完善了,可以大大减轻开发者入门的门槛。开发与测试开发与测试就是介绍如何来进行鸿蒙生态应用的开发与测试。从《白皮书》看出,鸿蒙生态应用的开发,将会力挺ArkTS语言。开发者如果之前没有接触ArkTS语言,建议可以关注下。上架与分发上架与分发部分就是介绍如何在开发完成之后,做上架与分发。自由流转与分布式运行自由流转与分布式运行是指鸿蒙生态应用的一个特色。因此,这章主要是介绍这个特色。运维分析运维分析主要是介绍如何来做鸿蒙生态应用的运维,遇到问题应该如何寻求解决。案例参考案例参考主要是给出了一些鸿蒙生态的几个应用场景,这样开发者能够直观了解鸿蒙生态最后能做什么。总结本质上这《白皮书》可以简单理解为是鸿蒙生态开发的一个总览或者是导读,让开发者可以快速去了解鸿蒙生态。“鸿蒙生态”应该是首次在官方文件中出现,这相当于是对“鸿蒙”定了性。之前,在各大媒体中经常能看到“鸿蒙”“OpenHarmony”“HarmonyOS”这些名词存在混用的情况,笔者也在博客《一文搞懂什么是鸿蒙、OpenHarmony、HarmonyOS》中对这些名词做了解释,在该博客中提到“鸿蒙生态”这个名词,不想与《白皮书》不谋而合,也算是一件趣事。最后,衷心祝愿鸿蒙生态越办越好,更多的开发者能进入这个生态,共创美好未来!笔者也提供了开源免费教程《跟老卫学HarmonyOS开发》欢迎指正!参考引用《一文搞懂什么是鸿蒙、OpenHarmony、HarmonyOS》https://waylau.com/what-is-harmonyos/《跟老卫学HarmonyOS开发》https://github.com/waylau/harmonyos-tutorial《鸿蒙HarmonyOS应用开发从入门到精通战》(柳伟卫著,北京大学出版社)https://item.jd.com/13696724.html>、
这是一台我个人DIY的第二代键盘主机(第一代见:https://www.bilibili.com/video/BV16Y4y1p7nv/)。相比与第一代的作品,第二代产品除了USB 3接口、USB 2接口、mini HDMI接口、DC电源接口外,拥有包括扬声器、话筒、3.5毫米耳机接口、TF卡槽等众多功能,因此,我称之为“全功能键盘主机”。主板主板是捡垃圾的 ,N3450低功耗CPU加持,板载6G内存+ 64 G固态,满足办公、轻度娱乐的需求,也适合做下载机。键盘同时,我还淘了个笔记本的键盘。键盘面板是需要切割和打磨成合适的宽度。外壳设计因为每块主板都是独一无二的,因此,外壳必须是量身定制。这边采用CAD软件来设计亚克力外壳。外壳制作我当然没有外壳制作的能力。把CAD图纸发给厂家,厂家做好就给寄过来了。这个外壳在设计时,便考虑了风扇位。安装最后就是将主板、键盘、外壳组装到一起啦。安装过程可以见视频:https://www.bilibili.com/video/BV1PW4y1r7d5/?vd_source=81ee817595c70f6444016cdcd4f62b06测试开机测试,成功点亮!用这个机子看视频,温度在60度左右,在没有开风扇的情况下,被动散热也还行。整体重量341g,跟一般的折叠屏手机差不多重啦。
老版的DevEco Studio只支持layout 资源类型的XML文件的预览。在新版的DevEco Studio 已经能够支持 Ability/AbilitySlice 的Java类文件的预览。新版的DevEco Studio 默认并不启用Java预览器,因此,选中Ability/AbilitySlice文件并切换预览器时,会包如下告警提示:To preview the Ability/AbilitySlice file, go to Settings > DevEco Labs > Enable Java Previewer.提示你需要去启用Java预览器。在DevEco Studio中,勾选“Settings > DevEco Labs > Enable Java Previewer”选项即可。以下是Java预览器的效果参考引用本系列归档至《跟老卫学 HarmonyOS 开发》:https://github.com/waylau/harmonyos-tutorial参考书籍《鸿蒙 HarmonyOS 应用开发从入门到精通》 :https://item.jd.com/13696724.html
HarmonyOS初探03——DevEco Studio创建应用问题ERROR Unable to tunnel through proxy. Proxy returns HTTP1.1 403DevEco Studio发表于 2020-12-09 09:27:31659查看问题在内网环境下首次使用DevEco Studio创建应用时,可能会报如下问题:ERROR: Unable to tunnel through proxy. Proxy returns "HTTP/1.1 403 Openproxy_Blocked_URL_list"我们已经是在DevEco Studio中设置了华为内网的网络proxy的。原因产生该问题的原因是,虽然设置了网络proxy,但该网络proxy并未包含Gradle的官方服务器。众所周知,DevEco Studio是采用Gradle来构建的。那么如何来解决?解决方案1:设置Gradle的网络proxy将Gradle的官方服务器纳入到网络proxy中来。方案2:设置Gradle内网服务器地址修改工程中gradle文件夹中的gradle-wrapper.properties文件中的distributionUrl的配置,将默认值https\://services.gradle.org/distributions/gradle-6.3-all.zip修改为其他内网地址,比如http://artifactory.cde.huawei.com/artifactory/gradle-distributions/gradle-6.5-all.zip控制台看到如下字样,说明Gradle已经能成功构建应用了。方案3:设置本地Gradle安装目录如果本地已经安装了Gradle,则可以直接使用本地的Gradle。参考引用本系列归档至《跟老卫学HarmonyOS开发》:https://github.com/waylau/harmonyos-tutorial参考书籍《鸿蒙HarmonyOS应用开发从入门到精通》(柳伟卫著,北京大学出版社出版):https://item.jd.com/13696724.html
虽然Java最新版本已经发展到Java 18了,但市面上大部分的项目还在使用Java 8。由于从Java 8之后,Java API不一定向前兼容,因此很多人都对升级Java版本心存顾虑。Java 11是Java 8的下一个长期支持版本,毫无疑问Java 11比Java 8更加优秀。本文介绍了将代码从 Java 8 转换到 Java 11用到的检查代码工具,还介绍了可能遇到的问题以及解决这些问题的建议。为什么需要Java 11Java 11是Java 8的下一个长期支持版本,这意味着Java 8不再受到官方支持。其外,从 Java 8 到Java 11,Java平台也发生了很大的更改,这些更改都是让Java平台更加优秀。本文重点介绍对性能、诊断和工作效率有影响的更改。模块模块解决在大型应用程序(在 classpath 上运行)中难以管理的配置和封装问题。 模块是 Java 类和接口以及相关资源的自述性集合。有了模块,即可自定义那些仅包含应用程序所需组件的运行时配置。 此自定义产生的内存占用量较小,因此可以使用 jlink 将应用程序静态链接到用于部署的自定义运行时中。 这个较小的内存占用量可能特别适用于微服务体系结构。在内部,JVM 可以通过让类加载更有效的方式利用模块。 结果就是,运行时更小、更轻便且启动速度更快。 JVM 用来改善应用程序性能的优化技术可以更有效,因为模块可以对某个类需要哪些组件进行编码。对程序员来说,模块可以要求显式声明一个模块可以导出哪些包以及它需要哪些组件,并且可以限制反射访问,因此有助于强制实施强封装。 这种级别的封装使应用程序更安全,维护起来更容易。应用程序可以继续使用 classpath,不需转换为作为必备组件的模块即可在 Java 11 上运行。Java 网络流量记录器Java Flight Recorder (JFR) 从正在运行的 Java 应用程序中收集诊断和分析数据。 JFR 对正在运行的 Java 应用程序几乎没有影响。 收集的数据随后可以使用 Java Mission Control (JMC) 和其他工具进行分析。 虽然 JFR 和 JMC 在 Java 8 中都是商业功能,但二者在 Java 11 中都是开放源代码。Java 任务控制java 任务控制 (JMC) 提供 java 网络流量记录器收集的数据的图形显示 (JFR) ,在 java 11 中是开放源代码。除了有关正在运行的应用程序的一般信息外,JMC 还允许用户向下钻取数据。 JFR 和 JMC 可以用来诊断运行时问题,例如内存泄露、GC 开销、热方法、线程瓶颈、阻塞 I/O。统一日志记录Java 11 有一个通用日志记录系统,适合 JVM 的所有组件。 用户可以使用此统一日志记录系统来定义哪些组件需要记录,以及记录到何种级别。 这种精细的日志记录适用于对 JVM 崩溃进行根本原因分析,以及在生产环境中诊断性能问题。低开销堆分析已经向 Java 虚拟机工具接口 (JVMTI) 添加了新的 API,用于对 Java 堆分配采样。 采样的开销低,可以持续启用。 虽然可以使用 Java Flight Recorder (JFR) 监视堆分配,但 JFR 中的采样方法只能用于分配。 JFR 实现也可能未命中分配。 与之形成对比的是,Java 11 中的堆采样可以同时提供活对象和死对象的相关信息。应用程序性能监视 (APM) 供应商开始利用此新功能,Java 工程组正在研究是否有可能将它与 Azure 性能监视工具配合使用。StackWalker进行日志记录时,通常会获取当前线程的堆栈的快照。 问题在于要记录多少堆栈跟踪,以及是否有必要记录堆栈跟踪。 例如,用户可能只想在某个方法出现特定异常时查看堆栈跟踪。 StackWalker 类(在 Java 9 中添加)提供堆栈的快照,并提供方便程序员对堆栈跟踪使用方式进行精细控制的方法。垃圾回收Java 11 提供以下垃圾回收器:Serial、Parallel、Garbage-First 和 Epsilon。 Java 11 中的默认垃圾回收器是 Garbage First 垃圾回收器 (G1GC)。此处提到其他三个回收器为了保持内容完整。 Z 垃圾回收器 (ZGC) 是一个并发、低延迟回收器,它会尝试将暂停时间保持在 10 毫秒以下。 ZGC 在 Java 11 中作为实验性功能提供。 Shenandoah 回收器是一个暂停时间短的回收器,它可以通过正在运行的 Java 程序以并发方式进行更多的垃圾回收,因此缩短了 GC 暂停时间。 Shenandoah 是 Java 12 中的一项实验性功能,但可以后向移植到 Java 11。 Concurrent Mark and Sweep (CMS) 回收器已发布,但自 Java 9 发布后已弃用。对于一般性使用,JVM 会将 GC 用作默认设置。 通常情况下,需根据应用程序的要求对这些设置和其他 GC 设置进行调整,以便优化吞吐量或延迟。 正确调整 GC 需要深入了解 GC,需要 Microsoft Java 工程组提供的专业知识。G1GCJava 11 中的默认垃圾回收器是 G1 垃圾回收器 (G1GC)。 G1GC 的目标是在延迟和吞吐量之间取得平衡。 G1 垃圾回收器尝试在大概率满足暂停时间目标的情况下实现高吞吐量目标。 G1GC 旨在避免整个集合,但当并发回收无法快速回收内存时,将发生回退完全 GC。 完全 GC 使用与初期混合性回收相同的并行工作线程数。并行 GC并行回收器是 Java 8 中的默认回收器。 并行 GC 是一个吞吐量回收器,使用多个线程来加速垃圾回收。EpsilonEpsilon 垃圾回收器负责处理分配,但不回收任何内存。 当堆耗尽时,JVM 会关闭。 Epsilon 适用于生存期短的服务和已知没有垃圾的应用程序。Docker 容器改进在 Java 10 之前,JVM 无法识别在容器上设置的内存和 CPU 约束。 例如,在 Java 8 中,JVM 会将最大堆大小默认设置为基础主机物理内存的四分之一。 从 Java 10 开始,JVM 会使用容器控制组 (cgroups) 设置的约束来设置内存和 CPU 限制(参见下面的说明)。 例如,默认的最大堆大小为容器的内存限制的四分之一(例如,如果内存限制为 2G,则最大堆大小为 500MB)。另外还添加了“JVM 选项”,使 Docker 容器用户可以精细地控制用于 Java 堆的系统内存量。此支持默认启用,仅在基于 Linux 的平台上提供。多版本 jar 文件在 Java 11 中,可以创建一个 jar 文件,其中包含多个特定于 Java 发布版的类文件版本。 有了多发布版 jar 文件,库开发人员就可以支持多个 Java 版本,不需交付多个版本的 jar 文件。 对于这些库的使用者来说,多发布版 jar 文件解决了必须将特定 jar 文件与特定运行时目标匹配的问题。其他性能改进对 JVM 进行以下更改会直接影响性能。JEP 197:分段代码缓存——将代码缓存分割成不同的段。 这种分段可以更好地控制 JVM 内存占用、缩短已编译方法的扫描时间、显著减轻代码缓存的碎片化,从而改进性能。JEP 254: Compact string——将字符串的内部表示形式从每个字符的两个字节更改为每个字符一个或两个字节,具体取决于字符编码。 由于大多数字符串包含 ISO-8859-1/拉丁语-1字符,此更改可以有效地将存储字符串所需的空间量减半。JEP 310:应用程序 Class-Data 共享-Class-Data共享通过允许在运行时进行内存映射来减少启动时间。 应用程序类-数据共享允许将应用程序类置于 CDS 存档中,从而扩展了类-数据共享。 当多个 JVM 共享同一存档文件时,可以节省内存并缩短总体的系统响应时间。JEP 312: Thread-Local 握手——使你能够在无需执行全局 VM safepoint 的情况下在线程上执行回调,这有助于 VM 减少全局 safepoints 的数量,从而实现较低的延迟。延迟分配编译器线程——在分层编译模式下,VM 将启动大量的编译器线程。 在有许多 CPU 的系统上,这是默认模式。 不管可用内存为多少,也不管编译请求有多少个,都会创建这些线程。 线程即使在空闲(几乎所有时间都是如此)的情况下也会耗用内存,这导致资源使用效率不高。 为了解决此问题,我们对实现进行了更改,在启动时每种类型只启动一个编译器线程。 系统会动态处理启动其他线程和关闭未使用线程的操作。对核心库进行以下更改会影响新代码或已修改代码的性能。JEP 193:变量句柄——定义一种标准方法,以调用对象字段和数组元素上的各种 util 和操作的等效操作,这是一组用于精确控制内存排序的标准围栏操作,也是一种标准的可访问性防护操作,以确保引用的对象保持可访问性。JEP 269:集合的便利工厂方法——定义库 api,使你可以轻松地创建包含少量元素的集合和映射的实例。 这是集合接口上的静态工厂方法,用于创建精简且不可修改的集合实例。 这些实例本质上更高效。 这些 API 创建的集合以简洁方式表示,没有包装器类。JEP 285: Spin-Wait 提示——提供 API,该 API 允许 Java 提示运行时系统处于自旋循环中。 某些硬件平台可以利用表明线程正处于“繁忙-等待”状态的软件指示。JEP 321: HTTP 客户端 (标准)——提供一个新的 http 客户端 API,该 API 可实现 Http/2 和 WebSocket,并可替换旧版 HttpURLConnection API。Java 8 转换到 Java 11可能的问题将代码从 Java 8 转换到 Java 11 时,并没有一种适用于所有情况的解决方案。 对于不重要的应用程序来说,从 Java 8 迁移到 Java 11 可能意味着很大的工作量。 潜在问题包括:删除的 API弃用的包内部 API 的使用对类加载程序的更改以及对垃圾回收的更改。通常,解决方法是尝试在不重新编译的情况下在 Java 11 上运行,或者先使用 JDK 11 进行编译。 如果目标是尽快启动并运行应用程序,则通常情况下,最佳方法是直接在 Java 11 上运行。 对于库,目标将是发布使用 JDK 11 编译和测试的项目。迁移到 Java 11 值得付出这样的努力。 自 Java 8 发布以来,已添加了多项新功能并对原有功能进行了强化。 这些功能和增强功能可改进启动、性能和内存使用情况,并提供与容器更好的集成。 此外还对 API 进行了添加和修改,这可以提高开发人员的工作效率。工具箱Java 11 有两个用于探查潜在问题的工具:jdeprscan 和 jdeps。 可以对现有类或 jar 文件运行这两个工具。 无需重新编译即可评估转换工作量。jdeprscan 可查看是否使用了已弃用或已删除的 API。 使用已弃用的 API 不是阻塞性问题,但值得探讨。 是否有更新的 jar 文件? 是否需要记录某个问题才能解决已弃用 API 的使用问题? 使用已删除的 API 是阻塞性问题,必须予以解决,然后才能尝试在 Java 11 上运行应用程序。jdeps,一个 Java 类依赖关系分析器。 与 --jdk-internals 选项一起使用时,jdeps 会告诉你哪个类依赖于哪个内部 API。 可以继续使用 Java 11 中的内部 API,但应优先考虑改变这种使用情况。 OpenJDK Wiki 页面 Java Dependency Analysis Tool(Java 依赖关系分析工具)推荐了某些常用 JDK 内部 API 的替换项。Gradle 和 Maven 都有 jdeps 和 jdeprscan 插件。 建议将以下工具添加到生成脚本中。工具Gradle 插件Maven 插件jdepsjdeps-gradle-pluginApache Maven JDeps 插件jdeprscanjdeprscan-gradle-pluginApache Maven JDeprScan 插件Java 编译器本身 javac 是工具箱中的另一个工具。 从 jdeprscan 和 jdeps 获取的警告和错误来自编译器。 使用 jdeprscan 和 jdeps 的优点是,可以在现有的 jar 和类文件(包括第三方库)上运行这两个工具。jdeprscan 和 jdeps 不能做的是对使用反射来访问封装的 API 进行警告。 反射访问在运行时进行检查。 最终必须在 Java 11 上运行代码才能确切地知道。使用 jdeprscan若要使用 jdeprscan,最简单的方法是为其提供一个来自现有生成的 jar 文件。 还可以为其指定目录(如编译器输出目录)或单个类名。 使用 --release 11 选项可获取已弃用 API 的最完整列表。 若要确定要采用的已弃用 API 的优先级,请将设置回退到 --release 8。 在 Java 8 中弃用的 API 的删除时间可能会早于最近弃用的 API。jdeprscan --release 11 my-application.jar如果无法解析依赖类,jdeprscan 工具会生成错误消息。 例如 error: cannot find class org/apache/logging/log4j/Logger。 建议将依赖类添加到 --class-path 或使用应用程序 class-path,但该工具会在没有它的情况下继续扫描。 参数是 -类路径。 class-path 参数的其他变体将不起作用。jdeprscan --release 11 --class-path log4j-api-2.13.0.jar my-application.jar error: cannot find class sun/misc/BASE64Encoder class com/company/Util uses deprecated method java/lang/Double::<init>(D)V此输出告诉我们,com.company.Util 类在调用 java.lang.Double 类的已弃用构造函数。 javadoc 会建议用来代替已弃用 API 的 API。 无论如何都无法解决“error: cannot find class sun/misc/BASE64Encoder”问题,因为它是已删除的 API。 自 Java 8 发布以来,应使用 java.util.Base64。运行 jdeprscan --release 11 --list 即可了解自 Java 8 后弃用的具体 API。 若要获取已删除 API 的列表,请运行 jdeprscan --release 11 --list --for-removal。使用 jdeps可以使用 jdeps 通过 --jdk-internals 选项来查找 JDK 内部 API 上的依赖项。 此示例需要 --multi-release 11 命令行选项,因为 log4j-core-2.13.0.jar 是多版本 jar 文件。 没有此选项,jdeps 会在找到多版本 jar 文件的情况下发出错误消息。 此选项指定要检查的类文件的版本。jdeps --jdk-internals --multi-release 11 --class-path log4j-core-2.13.0.jar my-application.jar Util.class -> JDK removed internal API Util.class -> jdk.base Util.class -> jdk.unsupported com.company.Util -> sun.misc.BASE64Encoder JDK internal API (JDK removed internal API) com.company.Util -> sun.misc.Unsafe JDK internal API (jdk.unsupported) com.company.Util -> sun.nio.ch.Util JDK internal API (java.base) Warning: JDK internal APIs are unsupported and private to JDK implementation that are subject to be removed or changed incompatibly and could break your application. Please modify your code to eliminate dependence on any JDK internal APIs. For the most recent update on JDK internal API replacements, please check: https://wiki.openjdk.java.net/display/JDK8/Java+Dependency+Analysis+Tool JDK Internal API Suggested Replacement ---------------- --------------------- sun.misc.BASE64Encoder Use java.util.Base64 @since 1.8 sun.misc.Unsafe See http://openjdk.java.net/jeps/260 输出提供了一些关于避免使用 JDK 内部 API 的好建议! 如果可能,建议使用替换 API。 在括号中提供封装了包的模块的名称。 如果需要显式中断封装,则可将模块名称与 --add-exports 或 --add-opens 配合使用。使用 sun.misc.BASE64Encoder 或 sun.misc.BASE64Decoder 会导致 Java 11 中出现 java.lang.NoClassDefFoundError。 使用这些 API 的代码必须经过修改才能使用 java.util.Base64。尝试不使用来自 jdk.unsupported 模块的任何 API。 此模块中的 API 将引用 JDK 增强方案 (JEP) 260 作为建议的替换方案。 简而言之,JEP 260 指出,在替换 API 可用之前,会一直支持使用内部 API。 虽然你的代码使用的是 JDK 内部 API,但至少在一段时间内它是可以正常运行的。 请看看 JEP 260,因为它指出了某些内部 API 的替换项。 例如,可以使用变量句柄来代替某个 sun.misc.Unsafe API。除了扫描 JDK 内部 API 的使用情况,jdeps 还可以执行其他操作。 它是一项有用的工具,可以用来分析依赖关系和生成模块信息文件。 有关详细信息,请参阅文档。使用 javac如果使用 JDK 11 进行编译,则需要更新才能生成脚本、工具、测试框架和包含的库。 使用 javac 的 -Xlint:unchecked 选项可获取 JDK 内部 API 的使用详情和其他警告。 可能还需要使用 --add-opens 或 --add-reads 向编译器公开封装的包(请参阅 JEP 261)。库可以考虑以多版本 jar 文件形式打包。 多版本 jar 文件允许同时支持同一 jar 文件中的 Java 8 和 Java 11 运行时。 它们增加了生成的复杂性。 如何生成多版本 jar 超出了本文档的讨论范围。在 Java 11 上运行大多数应用程序在不修改的情况下应该可以在 Java 11 上运行。 首先要尝试的是在不重新编译代码的情况下在 Java 11 上运行。 直接运行的目的是查看执行时会出现哪些警告和错误。 此方法可以让应用程序在 Java 11 上更快地运行,因为可以尽量减少那些必须完成的关注事项。你可能会遇到的大多数问题都可以得到解决,无需重新编译代码。 如果需要在代码中修复问题,请进行修复,但继续使用 JDK 8 进行编译。 如果可能,请在使用 JDK 11 进行编译之前,让应用程序使用 java 版本 11 运行。检查命令行选项在 Java 11 上运行之前,请对命令行选项进行快速扫描。 已删除的选项会导致 Java 虚拟机 (JVM) 退出。 如果使用 GC 日志记录选项,则此检查尤其重要,因为它们已明显不同于 Java 8 中的情况。 JaCoLine 工具是一项很好的工具,用于检查命令行选项的问题。检查第三方库你不能控制的第三方库是潜在的问题来源。 可以主动将第三方库更新到较新的版本。 也可查看运行应用程序时哪些库未使用,仅更新那些必需的库。 将所有库更新到最新版本的问题在于,如果应用程序中存在错误,则更难找到根本原因。 发生此错误是因为更新了某个库吗? 或者,此错误是由运行时中的某些更改引起的吗? 仅更新所需内容的问题在于,可能需要多次迭代才能解决问题。此处的建议是尽可能少做更改,将第三方库单独进行更新。 如果更新第三方库,则往往需要与 Java 11 兼容的最新且最好的版本。 根据当前版本的落后程度,你可能需要采取更谨慎的方法,升级到第一个与 Java 9+ 兼容的版本。除了查看发行说明以外,还可以使用 jdeps 和 jdeprscan 来评估 jar 文件。 此外,OpenJDK 质量组还维护一个质量外展 Wiki 页面,其中列出了根据 OpenJDK 版本对多个免费开源软件 (FOSS) 项目进行的测试的状态。显式设置垃圾回收并行垃圾回收器(并行 GC)是 Java 8 中的默认 GC。 如果应用程序使用默认值,则应使用命令行选项 -XX:+UseParallelGC 显式设置 GC。 Java 9 中的默认值已更改为 Garbage First 垃圾回收器 (G1GC)。 若要对 Java 8 与 Java 11 上运行的应用程序进行公平比较,GC 设置必须相同。 应推迟使用 GC 设置进行的试验,直到应用程序在 Java 11 上经过验证。显式设置默认选项如果在作用点 VM 上运行,则设置命令行选项 -XX:+PrintCommandLineFlags 会转储由 VM 设置的选项的值,特别是由 GC 设置的默认值。 在 Java 8 上使用此标志运行,在 Java 11 上运行时使用输出的选项。 大多数情况下,Java 8 到 11 中的默认值是相同的。 但是,使用 Java 8 中的设置可确保奇偶校验。建议设置命令行选项 --illegal-access=warn。 在 Java 11 中,使用反射访问 JDK 内部 API 会生成一个“非法的反射访问”警告。 默认情况下,系统仅对第一次非法访问发出警告。 设置 --illegal-access=warn 会导致系统对每一次非法反射访问发出警告。 如果将选项设置为 warn,则会发现更多非法访问案例。 但是,你也会收到大量冗余警告。在 Java 11 上运行应用程序后,设置 --illegal-access=deny 即可模拟 Java 运行时的未来行为。 从 Java 16 开始,默认设置将为 --illegal-access=deny。ClassLoader 注意事项在 Java 8 中,可以将系统类加载程序强制转换为 URLClassLoader。 这通常由需要在运行时将类注入到 classpath 的应用程序和库完成。 类加载程序层次结构在 Java 11 中已更改。 系统类加载程序(也称为应用程序类加载程序)现在是一个内部类。 强制转换为 URLClassLoader 会在运行时引发 ClassCastException。 Java 11 无法通过 API 在运行时动态增强 classpath,但可以通过反射来实现这一点,它会显示有关如何使用内部 API 的显著警告。在 Java 11 中,启动类加载程序只加载核心模块。 如果创建一个具有 null 父项的类加载程序,则它可能找不到全部平台类。 在 Java 11 中,需要在此类情况下传递 ClassLoader.getPlatformClassLoader() 而不是 null 作为父类加载程序。区域设置数据更改Java 11 中区域设置数据的默认源已通过 JEP 252 更改为 Unicode 联合会的公共区域设置数据存储库。 这可能会影响本地化的格式设置。 如有必要,请将系统属性设置为 java.locale.providers=COMPAT,SPI,还原到 Java 8 区域设置行为。潜在问题下面是可能会遇到的一些常见问题。无法识别的 VM 选项无法识别的选项VM 警告:忽略选项VM 警告:选项 已弃用警告:发生非法的反射访问操作java.lang.reflect.InaccessibleObjectExceptionjava.lang.NoClassDefFoundError-Xbootclasspath/p 不再是受支持的选项java.lang.UnsupportedClassVersionError无法识别的选项如果删除了某个命令行选项,则应用程序会输出 Unrecognized option: 或 Unrecognized VM option,后跟有问题的选项的名称。 无法识别的选项会导致 VM 退出。 已弃用但未删除的选项会生成 VM 警告。通常情况下,已删除的选项没有替换项,唯一办法是从命令行中删除该选项。 垃圾回收日志记录的选项是一个例外。 GC 日志记录已在 Java 9 中重新实现,可以使用统一 JVM 日志记录框架。 请参阅 Java SE 11 工具参考的允许通过 JVM 统一日志记录框架进行日志记录部分中的“表2-2 将旧的垃圾回收日志记录标志映射到 Xlog 配置”。VM 警告使用弃用的选项会生成警告。 当某个选项被替换或不再有用时,即表明它已被弃用。 与使用删除的选项一样,应从命令行中删除这些选项。 “VM Warning: Option was deprecated”警告意味着,该选项仍受支持,但以后可能会取消该支持。 不再受支持的选项会生成“VM Warning: Ignoring option”警告。 不再受支持的选项不影响运行时。Web 页面 VM 选项资源管理器提供了自 JDK 7 以后在 Java 中添加或删除的选项的详尽列表。错误:无法创建 Java 虚拟机当 JVM 遇到无法识别的选项时,会输出此错误消息。警告:发生非法的反射访问操作当 Java 代码使用反射访问 JDK 内部 API 时,运行时会发出“非法的反射访问”警告。WARNING: An illegal reflective access operation has occurred WARNING: Illegal reflective access by my.sample.Main (file:/C:/sample/) to method sun.nio.ch.Util.getTemporaryDirectBuffer(int) WARNING: Please consider reporting this to the maintainers of com.company.Main WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations WARNING: All illegal access operations will be denied in a future release这意味着,模块未导出通过反射访问的包。 此包在模块中封装,本质上是内部 API。 在 Java 11 上启动并运行应用程序时,第一项操作可能就是忽略此警告。 Java 11 运行时允许反射访问,因此旧代码可以继续运行。若要解决此警告,请查找不使用内部 API 的已更新代码。 如果无法使用更新的代码解决该问题,则可使用 --add-exports 或 --add-opens 命令行选项来启用对包的访问权限。 这些选项允许从一个模块访问另一个模块的未导出类型。--add-exports选项允许目标模块访问源模块的命名包的公共类型。 有时,代码会使用 setAccessible(true) 访问非公共成员和 API。 这称为深度反射。 在这种情况下,请使用 --add-opens,允许代码访问包的非公共成员。 如果不确定是使用 --add-exports 还是 --add-opens,请从 --add-exports 着手。应将 --add-exports 或 --add-opens 选项视为一种权宜解决方案,而不是长期解决方案。 使用这些选项会打破模块系统的封装,该封装是为了防止 JDK 内部 API 被使用。 如果删除或更改内部 API,应用程序会发生故障。 Java 16 会拒绝反射访问,但通过命令行选项(如 --add-opens)启用访问的情况除外。 若要模拟未来行为,请在命令行中设置 --illegal-access=deny。发出上述示例中的警告是因为 sun.nio.ch 包不是由 java.base 模块导出的。 换言之,模块 java.base 的 module-info.java 文件中没有 exports sun.nio.ch;。 这可以通过 --add-exports=java.base/sun.nio.ch=ALL-UNNAMED 来解决。 未在模块中定义的类隐式属于未命名模块,在字面上命名为 ALL-UNNAMED。java.lang.reflect.InaccessibleObjectException此异常指示你尝试在已封装类的字段或方法上调用 setAccessible(true)。 也可能会收到一个“非法的反射访问”警告。 使用 --add-opens 选项可以让代码访问包的非公共成员。 异常消息会告知你,模块未将包打开到试图调用 setAccessible 的模块。 如果模块是“未命名模块”,请使用 UNNAMED-MODULE 作为 --add-opens 选项中的目标模块。java.lang.reflect.InaccessibleObjectException: Unable to make field private final java.util.ArrayList jdk.internal.loader.URLClassPath.loaders accessible: module java.base does not "opens jdk.internal.loader" to unnamed module @6442b0a6 $ java --add-opens=java.base/jdk.internal.loader=UNNAMED-MODULE example.Mainjava.lang.NoClassDefFoundErrorNoClassDefFoundError 最有可能是由拆分包或引用删除的模块导致的。拆分包导致的 NoClassDefFoundError如果在多个库中找到某个包,则该包为拆分包。 拆分包问题的症状是,你知道某个类会在 class-path 上,但找不到该类。使用 module-path 时才会出现此问题。 Java 模块系统通过将包限制为一个命名的模块来优化类查找。 执行类查找时,运行时会优先处理 module-path 而不是 class-path。 如果包在某个模块和 class-path 之间拆分,则只使用该模块来执行类查找。 这可能导致 NoClassDefFound 错误。若要检查拆分包,一个简单的方法是将模块路径和类路径插入 jdeps,使用应用程序类文件的路径作为 。 如果有拆分包,jdeps 会输出警告:Warning: split package: <package-name> <module-path> <split-path>。可以通过使用 --patch-module <module-name>=<path>[,<path>] 将拆分包添加到命名模块中来解决此问题。使用 Java EE 或 CORBA 模块导致的 NoClassDefFoundError如果应用程序在 Java 8 上运行但却引发 java.lang.NoClassDefFoundError 或 java.lang.ClassNotFoundException,则可能是应用程序在使用 Java EE 或 CORBA 模块中的包。 这些模块在 Java 9 弃用,在 Java 11 中删除。若要解决此问题,请向项目添加运行时依赖项。删除的模块受影响的包建议的依赖项Java API for XML Web Services (JAX-WS)java.xml.wsJAX WS RI 运行时用于 XML 绑定的 Java 体系结构 (JAXB)java.xml.bindJAXB 运行时JavaBeans Activation Framework (JAV)java.activationJavaBeans (TM) Activation Framework常见批注java.xml.ws.annotationJavax 批注 API通用对象请求代理体系结构 (CORBA)java.corbaGlassFish CORBA ORBJava 事务 API (JTA)java.transactionJava 事务 API-Xbootclasspath/p 不再是受支持的选项已删除对 -Xbootclasspath/p 的支持。 请改用 --patch-module。 --patch-module 选项在 JEP 261 中介绍。 查找标为“修补模块内容”的部分。 可以将 --patch-module 与 javac 和 java 配合使用,以便重写或增强模块中的类。实际上,--patch-module 执行的操作是将修补模块插入模块系统的类查找。 模块系统会首先从修补模块获取类。 这与在 Java 8 中预挂起 bootclasspath 的效果相同。UnsupportedClassVersionError此异常表示你尝试在较低版本的 Java 上运行使用较高版本的 Java 编译的代码。 例如,在 Java 11 上运行其 jar 是使用 JDK 13 编译的程序。Java 版本类文件格式版本8529531054115512561357后续步骤在 Java 11 上运行应用程序后,请考虑将库移出 class-path,然后再将其移入 module-path。 查找应用程序所依赖的库的已更新版本。 选择模块库(如果可用)。 尽可能使用 module-path,即使不打算在应用程序中使用模块。 与 class-path 相比,使用module-path 可以获得更好的类加载性能。参考引用原文同步至:https://waylau.com/update-from-java-8-to-java-11https://docs.microsoft.com/zh-cn/java/openjdk/transition-from-java-8-to-java-11更多示例可见《现代Java案例大全》:https://github.com/waylau/modern-java-demos柳伟卫《Java核心编程》:https://search.jd.com/Search?keyword=%E6%9F%B3%E4%BC%9F%E5%8D%AB%20Java%E6%A0%B8%E5%BF%83%E7%BC%96%E7%A8%8B&enc=utf-8&wq=%E6%9F%B3%E4%BC%9F%E5%8D%AB%20Java%E6%A0%B8%E5%BF%83%E7%BC%96%E7%A8%8B&pvid=3f8660921bef4700931a735f536eebfb
很多语言都有类似于“虚拟线程”的技术,比如Go、C#、Erlang、Lua等,他们称之为“协程”。 不管是虚拟线程还是协程,他们都是轻量级线程,其目的都是为了提高并发能力。 本节详细介绍Java平台的“虚拟线程”的技术——“JEP 425: Virtual Threads (Preview)”。Java平台计划引入虚拟线程,可显著减少编写、维护和观察高吞吐量并发应用程序的工作量。“JEP 425: Virtual Threads (Preview)”目是一个预览性的API。目标使以简单的线程每请求风格编写的服务器应用程序能够以近乎最佳的硬件利用率进行扩展。启用使用java.lang.Thread API的现有代码,以最小的更改采用虚拟线程。使用现有的JDK工具,轻松地对虚拟线程进行故障排除、调试和分析。非目标目标不是删除线程的传统实现,也不是静默迁移现有应用程序以使用虚拟线程。改变Java的基本并发模型并不是目标。在Java语言或Java库中提供新的数据并行结构并不是目标。Stream API仍然是并行处理大型数据集的首选方式。动机近30年来,Java开发人员一直依赖线程作为并发服务器应用程序的构建块。每个方法中的每个语句都在线程内执行,由于Java是多线程的,多个执行线程同时发生。线程是Java的并发单元:一段顺序代码,与其他此类单元同时运行,而且在很大程度上独立于其他此类单元。每个线程都提供一个堆栈来存储局部变量和协调方法调用,以及出错时的上下文:异常被同一线程中的方法抛出和捕获,因此开发人员可以使用线程的堆栈跟踪来查找发生了什么。线程也是工具的核心概念:调试器逐步浏览线程方法中的语句,分析器可视化多个线程的行为,以帮助了解它们的性能。线程每请求样式服务器应用程序通常处理相互独立的并发用户请求,因此应用程序通过在请求的整个持续时间内将线程专用于该请求来处理请求是有意义的。这种线程每请求风格易于理解、易于编程、易于调试和分析,因为它使用平台的并发单位来表示应用程序的并发单位。服务器应用程序的可扩展性遵循利特尔定律(Little's Law),它与延迟、并发和吞吐量有关:对于给定的请求处理持续时间(即延迟),应用程序同时处理的请求数(即,并发)必须与到达速率(即吞吐量)成比例增长。例如,假设平均延迟为50ms的应用程序通过同时处理10个请求,实现每秒200个请求的吞吐量。为了使该应用程序扩展到每秒2000个请求的吞吐量,它需要同时处理100个请求。如果每个请求在请求的持续时间内都在线程中处理,那么,要使应用程序跟上,线程数量必须随着吞吐量的增长而增长。不幸的是,可用线程的数量是有限的,因为JDK将线程作为操作系统(OS)线程的包装器实现。操作系统线程成本高昂,因此我们不能拥有太多线程,这使得实现不适合线程每请求风格。如果每个请求在其持续时间内消耗一个线程,从而消耗一个操作系统线程,那么线程数量通常在其他资源(如CPU或网络连接)耗尽之前很久就成为限制因素。JDK当前的线程实现将应用程序的吞吐量限制在远低于硬件所能支持的水平。即使线程池化,也会发生这种情况,因为池化有助于避免启动新线程的高成本,但不会增加线程总数。使用异步风格提高可扩展性一些希望充分利用硬件的开发人员放弃了每请求线程风格,转到线程共享风格。请求处理代码不是从头到尾处理一个线程上的请求,而是在等待I/O操作完成时将其线程返回到池,以便线程可以为其他请求提供服务。这种细粒度的线程共享--在这种共享中,代码仅在线程执行计算时保留线程,而不是在等待I/O时保留线程--允许大量并发操作,而不会消耗大量线程。虽然它消除了操作系统线程稀缺性对吞吐量的限制,但它的代价很高:它需要所谓的异步编程风格,使用一组单独的I/O方法,这些方法不等待I/O操作完成,而是,稍后,向回调表示它们的完成。如果没有专用线程,开发人员必须将其请求处理逻辑分解为小阶段,通常编写为lambda表达式,然后使用API将它们组合成顺序管道(请参见CompletableFuture,或所谓的“反应性”(reactive)框架。因此,它们放弃了语言的基本顺序组成运算符,如循环和try/catch块。在异步风格中,请求的每个阶段都可能在不同的线程上执行,每个线程以交错的方式运行属于不同请求的阶段。这对理解程序行为具有深刻的影响:堆栈跟踪不提供可用的上下文,调试器无法逐步完成请求处理逻辑,分析器无法将操作的成本与其调用者关联起来。在使用Java时,编写lambda表达式是可以管理的Stream API在短管道中处理数据,但当应用程序中的所有请求处理代码都必须以这种方式编写时,就会有问题。这种编程风格与Java平台不一致,因为应用程序的并发单位--异步管道--不再是平台的并发单位。使用虚拟线程保留线程每请求样式为了使应用程序能够扩展,同时与平台保持和谐,我们应该通过更有效地实现线程来努力保留每个请求的线程风格,这样它们就可以更丰富。操作系统无法更有效地实现操作系统线程,因为不同的语言和运行时以不同的方式使用线程堆栈。但是,Java运行时可以以一种将Java线程与操作系统线程的一对一对应关系分开的方式实现Java线程。就像操作系统通过将大量虚拟地址空间映射到有限数量的物理RAM来给人丰富内存的错觉一样,Java运行时也可以通过将大量虚拟线程映射到少量操作系统线程来给人丰富线程的错觉。虚拟线程是java.lang.Thread的一个实例,它不绑定到特定的操作系统线程。相比之下,平台线程是java.lang.Thread的实例,以传统方式实现,作为操作系统线程周围的精简包装器。线程每请求样式中的应用程序代码可以在虚拟线程中运行整个请求持续时间,但虚拟线程仅在CPU上执行计算时消耗操作系统线程。结果是与异步风格相同的可扩展性,只是它是透明的:当在虚拟线程中运行的代码调用java中的阻塞I/O操作的java.* API时,运行时执行非阻塞操作系统调用,并自动挂起虚拟线程,直到稍后可以恢复。对于Java开发人员来说,虚拟线程只是创建起来很便宜,而且几乎无限丰富的线程。硬件利用率接近最佳,允许高水平的并发性,从而实现高吞吐量,同时应用程序与Java平台及其工具的多线程设计保持和谐。虚拟线程的含义虚拟线程既便宜又丰富,因此永远不应该池化:应该为每个应用程序任务创建一个新的虚拟线程。因此,大多数虚拟线程都是短暂的,并且具有浅调用堆栈,执行的时间只需单个HTTP客户端调用或单个JDBC查询。相比之下,平台线程是重量级和昂贵的,因此通常必须池化。它们往往是长寿命的,具有深度的调用堆栈,并在许多任务中共享。总之,虚拟线程保留了可靠的每请求线程风格,该风格与Java平台的设计和谐,同时优化地利用硬件。使用虚拟线程不需要学习新的概念,尽管它可能需要养成不学习的习惯,以应对当今线程的高成本。虚拟线程不仅将帮助应用程序开发人员,还将帮助框架设计人员提供易于使用的API,这些API与平台的设计兼容,而不影响可扩展性。描述今天,每一个例子 java.lang.Thread在JDK中,是一个平台线程。平台线程在底层操作系统线程上运行Java代码,并在代码的整个生命周期内捕获操作系统线程。平台线程数限制为操作系统线程数。虚拟线程是java.lang.Thread的一个实例,它在基础操作系统线程上运行Java代码,但在代码的整个生命周期内不捕获操作系统线程。这意味着许多虚拟线程可以在同一操作系统线程上运行其Java代码,有效地共享它。虽然平台线程垄断了宝贵的操作系统线程,但虚拟线程却不垄断。虚拟线程的数量可以远大于操作系统线程的数量。虚拟线程是由JDK而不是操作系统提供的线程的轻量级实现。它们是用户模式线程的一种形式,在其他多线程语言中已经成功(例如,Go中的goroutine和Erlang中的进程)。用户态线程甚至被称为“green threads”在Java的早期版本中,当时操作系统线程还不成熟和广泛。然而,Java的green threads都共享一个操作系统线程(M:1调度),平台线程被实现为操作系统线程的包装器(1:1调度)。虚拟线程采用M:N调度,其中大量(M)虚拟线程被调度在较少数量(N)的操作系统线程上运行。使用虚拟线程vs平台线程开发者可以选择使用虚拟线程还是平台线程。这里是一个创建大量虚拟线程的示例程序。程序首先获取一个ExecutorService这将为每个提交的任务创建一个新的虚拟线程。然后,它提交10,000个任务,并等待所有任务完成:try (var executor = Executors.newVirtualThreadPerTaskExecutor()) { IntStream.range(0, 10_000).forEach(i -> { executor.submit(() -> { Thread.sleep(Duration.ofSeconds(1)); return i; }); }); } // executor.close() is called implicitly, and waits本例中的任务是简单的代码--睡眠一秒钟--现代硬件可以轻松支持10000个虚拟线程同时运行此类代码。在幕后,JDK在少量的操作系统线程上运行代码,也许只有一个线程。如果此程序使用为每个任务创建新的平台线程的ExecutorService,如Executor.newCachedThreadPool(),情况就会大不相同。ExecutorService将尝试创建10000个平台线程,从而创建10000个操作系统线程,并且该程序将在大多数操作系统上崩溃。相反,如果程序使用从池获取平台线程的ExecutorService,如Executor.newFixedThreadPool(200),情况也不会好太多。ExecutorService将创建200个平台线程,供所有10000个任务共享,因此许多任务将顺序运行,而不是并发运行,程序将需要很长时间才能完成。对于此程序,具有200个平台线程的池只能实现每秒200个任务的吞吐量,而虚拟线程的吞吐量则约为每秒10000个任务(在充分预热后)。此外,如果示例程序中的10_000更改为1_000_000,则程序将提交100万个任务,创建100万个同时运行的虚拟线程,并(在充分预热后)实现每秒约100万个任务的吞吐量。如果此程序中的任务执行了一秒钟的计算(例如,排序一个巨大的数组),而不仅仅是睡眠,那么将线程数量增加到处理器核心数量之外都没有帮助,不管它们是虚拟线程还是平台线程。虚拟线程不是更快的线程-它们运行代码的速度并不比平台线程快。它们的存在是为了提供规模(更高的吞吐量),而不是速度(更低的延迟)。它们可能比平台线程多得多,因此根据利特尔定律,它们实现了更高吞吐量所需的更高并发。换一种方式说,虚拟线程可以显著提高应用程序吞吐量,当并发任务数量较高(超过几千个),并且工作负载不绑定CPU,因为在这种情况下,线程数比处理器内核多得多不能提高吞吐量。虚拟线程有助于提高典型服务器应用程序的吞吐量,正是因为此类应用程序由大量并发任务组成,这些任务花费了大量时间等待。虚拟线程可以运行平台线程可以运行的任何代码。特别是,虚拟线程支持线程局部变量和线程中断,就像平台线程一样。这意味着处理请求的现有Java代码将很容易在虚拟线程中运行。许多服务器框架将选择自动执行此操作,为每个传入请求启动一个新的虚拟线程,并在其中运行应用程序的业务逻辑。以下是一个服务器应用程序的示例,它聚合了其他两个服务的结果。假设的服务器框架(未显示)为每个请求创建一个新的虚拟线程,并在该虚拟线程中运行应用程序的句柄代码。应用程序代码反过来创建两个新的虚拟线程,通过与第一个示例相同的ExecutorService并发获取资源:void handle(Request request, Response response) { var url1 = ... var url2 = ... try (var executor = Executors.newVirtualThreadPerTaskExecutor()) { var future1 = executor.submit(() -> fetchURL(url1)); var future2 = executor.submit(() -> fetchURL(url2)); response.send(future1.get() + future2.get()); } catch (ExecutionException | InterruptedException e) { response.fail(e); } } String fetchURL(URL url) throws IOException { try (var in = url.openStream()) { return new String(in.readAllBytes(), StandardCharsets.UTF_8); } }像这样的服务器应用程序,具有简单的阻塞代码,可以很好地扩展,因为它可以使用大量的虚拟线程。Executor.newVirtualThreadPerTaskExecutor()并不是创建虚拟线程的唯一方法。新的java.lang.Thread.BuilderAPI,下面讨论,可以创建和启动虚拟线程。此外,结构化并发提供了一个更强大的API来创建和管理虚拟线程,特别是在类似于此服务器示例的代码中,通过该API,线程之间的关系将被平台及其工具所知道。虚拟线程是一个预览API,默认禁用上面的程序使用Executors.newVirtualThreadPerTaskExecutor()方法,因此要在JDK XX上运行它们,必须按以下方式启用预览API:使用javac --release XX --enable-preview Main.java编译程序,并使用java--enable-preview Main运行程序;或,当使用源代码启动器,使用java --release XX --enable-preview Main.java;运行程序;或,使用时jshell,以jshell --enable-preview开头不池化虚拟线程开发人员通常会将应用程序代码从基于线程池的传统ExecutorService迁移到虚拟线程每任务ExecutorService。线程池和所有资源池一样,旨在共享昂贵的资源,但虚拟线程并不昂贵,而且永远不需要将它们池化。开发人员有时使用线程池来限制对有限资源的并发访问。例如,如果一个服务不能处理超过20个并发请求,则通过提交到大小为20的池的任务执行对该服务的所有访问将确保这一点。由于平台线程的高成本使线程池无处不在,这种成语也变得无处不在,但开发人员不应该被诱惑池虚拟线程以限制并发。专门为此目的设计的构造,如信号量,应用于保护对有限资源的访问。这比线程池更有效和方便,也更安全,因为线程本地数据不会意外从一个任务泄露到另一个任务的风险。观察虚拟线程编写清晰的代码并不是完整的故事。清楚地表示正在运行的程序的状态对于故障排除、维护和优化也是必不可少的,JDK长期以来一直提供调试、分析和监控线程的机制。这些工具应该对虚拟线程执行同样的操作--也许可以对其大量的操作进行一些调节--因为它们毕竟是java.lang.Thread的实例。Java调试器可以逐步浏览虚拟线程、显示调用堆栈和检查堆栈帧中的变量。JDK Flight Recorder (JFR)是JDK的低开销分析和监控机制,可以将应用程序代码中的事件(如对象分配和I/O操作)与正确的虚拟线程关联。这些工具不能为以异步样式编写的应用程序执行这些操作。在这种风格中,任务与线程无关,因此调试器无法显示或操作任务的状态,分析器也无法判断任务等待I/O的时间。线程转储是另一个流行的工具,用于对以线程每请求风格编写的应用程序进行故障排除。不幸的是,JDK的传统线程转储,使用jstack或jcmd获得,提供了一个线程的扁平列表。这适合几十个或数百个平台线程,但不适合数千个或数百万个虚拟线程。因此,我们不会扩展传统的线程转储以包括虚拟线程,而是在jcmd中引入一种新的线程转储,以将虚拟线程与平台线程一起呈现,所有这些线程都以有意义的方式分组。当程序使用时,可以显示出线程之间更丰富的关系结构化并发。由于可视化和分析大量线程可以从工具中受益,jcmd除了纯文本外,还可以以JSON格式发出新的线程转储:$ jcmd <pid> Thread.dump_to_file -format=json <file>新的线程转储格式列出了在网络I/O操作中被阻止的虚拟线程,以及由上面所示的new-thread-per-task(一任务一线程)ExecutorService创建的虚拟线程。它不包括对象地址、锁、JNI统计信息、堆统计信息和传统线程转储中出现的其他信息。此外,由于它可能需要列出大量线程,因此生成新的线程转储不会暂停应用程序。以下是这样的线程转储的示例,取自与上面第二个示例类似的应用程序,在JSON查看器中呈现(见下图):由于虚拟线程是在JDK中实现的,并且不绑定到任何特定的操作系统线程,因此它们对操作系统是不可见的,而操作系统不知道它们的存在。操作系统级监控将观察到JDK进程使用的操作系统线程比虚拟线程少。调度虚拟线程要做有用的工作,需要调度线程,即分配在处理器核心上执行。对于作为操作系统线程实现的平台线程,JDK依赖于操作系统中的调度程序。相比之下,对于虚拟线程,JDK有自己的调度程序。JDK的调度程序将虚拟线程分配给平台线程,而不是直接将虚拟线程分配给处理器(这就是前面提到的虚拟线程的M:N调度)。然后,操作系统将像往常一样调度平台线程。JDK的虚拟线程调度程序是一个窃取工作的工具ForkJoinPool在FIFO模式下工作。调度程序的并行性是可用于调度虚拟线程的平台线程数。默认情况下,它等于可用处理器,但它可以使用系统属性jdk.virtualThreadScheduler.parallelism进行调整。请注意,此ForkJoinPool不同于公共池,例如,它用于并行流的实现,并在后进先出模式下工作。调度程序分配虚拟线程的平台线程称为虚拟线程的载体。虚拟线程可以在其生命周期内在不同的载体上调度;换句话说,调度程序不保持虚拟线程和任何特定平台线程之间的亲和性。但是,从Java代码的角度来看,正在运行的虚拟线程在逻辑上独立于其当前载体:虚拟线程无法使用运营商的身份。Thread.currentThread()返回的值始终是虚拟线程本身。载体和虚拟线程的堆栈跟踪是分开的。虚拟线程中抛出的异常将不包括载体的堆栈帧。线程转储不会在虚拟线程的堆栈中显示载体的堆栈帧,反之亦然。载体的线程局部变量对虚拟线程不可用,反之亦然。此外,从Java代码的角度来看,虚拟线程及其载体暂时共享操作系统线程的事实是不可见的。相比之下,从本机代码的角度来看,虚拟线程和它的载体都运行在同一个本机线程上。因此,在同一虚拟线程上多次调用的本机代码可能会在每次调用时观察到不同的操作系统线程标识符。调度程序当前不为虚拟线程实现分时。分时是对占用分配数量的CPU时间的线程的强制抢占。虽然分时使用几百个平台线程可以有效,但目前还不清楚分时使用一百万个虚拟线程是否会那么有效。执行虚拟线程要利用虚拟线程,没有必要重写程序。虚拟线程不要求或期望应用程序代码显式地将控制权交回调度程序;换句话说,虚拟线程是不合作的。用户代码不得假设虚拟线程如何或何时分配给平台线程,就如假设平台线程如何或何时分配给处理器内核一样。要在虚拟线程中运行代码,JDK的虚拟线程调度程序通过将虚拟线程挂载在平台线程上,分配虚拟线程在平台线程上执行。这使得平台线程成为虚拟线程的载体。稍后,在运行一些代码后,虚拟线程可以从其载体上卸载。在这一点上,平台线程是空闲的,因此调度程序可以在其上装载不同的虚拟线程,从而使其再次成为载体。通常,当虚拟线程阻塞I/O或JDK中的其他阻塞操作时,虚拟线程将卸载,如BlockingQueue.take()。当阻塞操作准备完成时(例如,已在套接字上接收字节),它将虚拟线程提交回调度程序,调度程序将在载体上装载虚拟线程以恢复执行。虚拟线程的装载和卸载频繁且透明地进行,并且不会阻止任何操作系统线程。例如,前面显示的服务器应用程序包括以下代码行,其中包含对阻止操作的调用:response.send(future1.get() + future2.get());这些操作将导致虚拟线程多次装载和卸载,通常每次调用get()一次,在send(...)中执行I/O过程中可能多次。JDK中的绝大多数阻塞操作将卸载虚拟线程,释放其载体和底层操作系统线程来承担新的工作。但是,JDK中的一些阻塞操作不会卸载虚拟线程,因此会阻塞其载体和底层操作系统线程。这是因为操作系统级别(例如,许多文件系统操作)或JDK级别(例如,Object.wait())。这些阻塞操作的实现将通过临时扩展调度程序的并行性来补偿操作系统线程的捕获。因此,调度程序的ForkJoinPool中的平台线程数量可能暂时超过可用处理器的数量。调度程序可用的最大平台线程数可以使用系统属性jdk.virtualThreadScheduler.maxPoolSize进行调整。在两种情况下,虚拟线程在阻塞操作期间无法卸载,因为它被固定在其载体上:当它在同步块或方法中执行代码时,或当它执行本机方法或外来函数.固定不会使应用程序不正确,但可能会妨碍其可扩展性。如果虚拟线程在被固定时执行阻塞操作,如I/O或BlockingQueue.take(),则在操作期间,其载体和基础操作系统线程将被阻塞。长时间频繁固定可能会通过捕获运营商而损害应用程序的可扩展性。调度程序不会通过扩展其并行度来补偿固定。相反,通过修改频繁运行的同步块或方法,并保护要使用的潜在长I/O操作,避免频繁和长期的固定java.util.concurrent.locks.ReentrantLock相反,不需要替换不经常使用的同步块和方法(例如,仅在启动时执行)或保护内存操作。和往常一样,努力保持锁定策略简单明了。新的诊断有助于将代码迁移到虚拟线程,并评估是否应将同步的特定使用替换为java.util.concurrent锁:当线程在被固定时阻塞时,将发出JDK Flight Recorder (JFR) 事件.系统属性jdk.tracePinnedThreads在线程被固定时阻塞时触发堆栈跟踪。使用-Djdk.tracePinnedThreads=full运行时,当线程在固定时阻塞时,打印完整的堆栈跟踪,本机帧和持有监视器的帧高亮显示。使用-Djdk.tracePinnedThreads=short将输出限制为仅有问题的帧。我们也许能够在未来的版本中删除上面的第一个限制。第二个限制是与本机代码正确交互所必需的。内存使用和与垃圾回收的交互虚拟线程的堆栈作为堆栈块对象存储在Java的垃圾收集堆中。堆栈随着应用程序运行而增长和收缩,既是为了提高内存效率,又是为了容纳任意深度的堆栈(最高可达JVM配置的平台线程堆栈大小)。这种效率使大量虚拟线程得以实现,从而使服务器应用程序中线程每请求风格的持续生存能力得以实现。在上面的第二个示例中,请记住,假设框架通过创建新的虚拟线程并调用句柄方法来处理每个请求;即使它在深度调用堆栈的末尾调用句柄(在身份验证、事务等之后),句柄本身会生成多个仅执行短期任务的虚拟线程。因此,对于每个具有深调用栈的虚拟线程,将有多个具有浅调用栈的虚拟线程消耗很少的内存。一般来说,虚拟线程所需的堆空间和垃圾收集器活动量很难与异步代码相比。处理请求的应用程序代码通常必须跨I/O操作维护数据;线程每请求代码可以将数据保留在局部变量中,局部变量存储在堆中的虚拟线程堆栈上。而异步代码必须在堆对象中保留相同数据,这些数据从管道的一个阶段传递到下一个阶段。一方面,虚拟线程所需的堆栈帧布局比紧凑对象更浪费;另一方面,虚拟线程在许多情况下(取决于低级GC交互)可以变异和重用其堆栈,而异步管道总是需要分配新对象。总体而言,线程每请求与异步代码的堆消耗和垃圾收集器活动应该大致相似。随着时间的推移,我们希望使虚拟线程堆栈的内部表示更加紧凑。与平台线程栈不同,虚拟线程栈不是GC根,因此其中包含的引用不会被执行并发扫描的垃圾收集器在停止世界暂停中遍历。这也意味着,如果虚拟线程在上被阻塞,例如 BlockingQueue.take(),并且没有其他线程可以获得对虚拟线程或队列的引用,那么该线程可以被垃圾收集-这很好,因为虚拟线程永远不会被中断或解除阻塞。当然,如果虚拟线程正在运行,或者如果它被阻止,并且可能会被取消阻止,则它不会被垃圾收集。虚拟线程的当前限制是G1 GC不支持巨大的堆栈块对象。如果虚拟线程的堆栈达到区域大小的一半(通常为512KB),则可能会引发StackOverflowError。详细更改其余小节详细介绍了在Java平台及其实现中提出的更改:java.lang.ThreadThread-local variablesjava.util.concurrentNetworkingjava.ioJava Native Interface (JNI)Debugging (JVM TI, JDWP, and JDI)JDK Flight Recorder (JFR)Java Management Extensions (JMX)java.lang.ThreadGroupjava.lang.Thread更新 java.lang.Thread API如下:Thread.Builder、Thread.ofVirtual()和Thread.ofPlatform()是用于创建虚拟线程和平台线程的新API。例如,Thread thread = Thread.ofVirtual().name("duke").unstarted(runnable);创建一个名为“duke”的新未启动虚拟线程。Thread.startVirtualThread(Runnable)是创建和启动虚拟线程的方便方法。Thread.Builder可以创建一个线程,也可以创建一个ThreadFactory,然后可以创建具有相同属性的多个线程。Thread.isVirtual()测试线程是否为虚拟线程。Thread.join 和 Thread.sleep接受等待和睡眠时间作为java.time.Duration.新的最终方法Thread.threadId() 返回线程的标识符。现有非最终方法 Thread.getId() 现在已弃用。Thread.getAllStackTraces() 现在返回所有平台线程而不是所有线程的映射。java.lang.Thread API在其他方面保持不变。线程类定义的构造函数创建平台线程,与前面一样。没有新的公共构造函数。虚拟线程和平台线程之间的主要API区别是:公共线程构造函数无法创建虚拟线程。虚拟线程始终是守护线程。Thread.setDaemon(boolean) 方法无法将虚拟线程更改为非守护线程。虚拟线程的固定优先级为 Thread.NORM_PRIORITY。 Thread.setPriority(int) 方法对虚拟线程没有影响。此限制可能会在未来的版本中重新讨论。虚拟线程不是线程组的活动成员。在虚拟线程上调用时,Thread.getThreadGroup() 返回名为“VirtualThreads”的占位线程组。Thread.Builder API没有定义设置虚拟线程线程的线程组的方法。使用SecurityManager集运行时,虚拟线程没有权限。虚拟线程不支持 stop()、suspend()、 resume()方法。这些方法在虚拟线程上调用时引发异常。线程局部变量虚拟线程支持线程局部变量(ThreadLocal)和可继承的线程局部变量(InheritableThreadLocal),就像平台线程一样,因此它们可以运行使用线程本地程序的现有代码。但是,由于虚拟线程可能非常多,请在仔细考虑后使用线程本地。特别是,不要使用线程本地在线程池中共享同一线程的多个任务之间汇集昂贵的资源。虚拟线程不应池化,因为每个线程在其生命周期内只运行一个任务。我们已经从java.base模块中删除了线程本地的许多使用,以准备虚拟线程,以减少在使用数百万线程运行时的内存占用。此外:Thread.Builder API定义了创建线程时选择退出线程本地的方法。它还定义了选择退出继承可继承线程局部的初始值的方法。当从不支持线程本地的线程调用时,ThreadLocal.get() 返回初始值和线程本地集(T)抛出异常。遗产上下文类加载器现在指定为像本地可继承线程一样工作。如果Thread.setContextClassLoader(ClassLoader)在不支持线程本地的线程上调用,然后引发异常。作用域-局部变量对于某些用例,可能会被证明是线程本地的更好替代方案。java.util.concurrent支持锁定的原始API。java.util.concurrent.LockSupport,现在支持虚拟线程:驻留虚拟线程释放基础载体线程以执行其他工作,取消驻留虚拟线程计划它继续。对LockSupport的此更改使所有使用它的API(锁、信号量、阻塞队列等)在虚拟线程中调用时都能优雅地停放。此外:Executors.newThreadPerTaskExecutor(ThreadFactory) 和 Executors.newVirtualThreadPerTaskExecutor() 创建一个ExecutorService,为每个任务创建一个新线程。这些方法支持迁移和与使用线程池和ExecutorService的现有代码的互操作性。执行器服务现在延伸可自动关闭,因此允许此API与上面示例中所示的try-with-resource构造一起使用。Future现在定义了获取已完成任务的结果或异常以及获取任务状态的方法。结合起来,这些添加使我们可以很容易地将Future对象用作流的元素,过滤Future流以查找已完成的任务,然后映射以获得结果流。这些方法也将有助于为结构化并发。网络java.net和java.nio.channels包中的网络API的实现现在与虚拟线程一起工作:对虚拟线程的操作,该操作阻止建立网络连接或从套接字读取,释放基础载体线程以执行其他工作。为了允许中断和取消,阻塞I/O方法定义为java.net.Socket、ServerSocket和DatagramSocket现在指定为可中断的在虚拟线程中调用时:中断套接字上阻塞的虚拟线程将取消锁定线程并关闭套接字。阻止这些类型套接字上的I/O操作,当从 InterruptibleChannel一直是可中断的,因此此更改将这些API在创建时的行为与从通道获取时的行为对齐。java.iojava.io包提供字节流和字符流的API。这些API的实现是高度同步的,需要进行更改,以避免在虚拟线程中使用它们时固定。作为背景,面向字节的输入/输出流没有被指定为线程安全,也没有指定当线程在读或写方法中被阻塞时调用close()时的预期行为。在大多数情况下,使用来自多个并发线程的特定输入或输出流是没有意义的。面向字符的读取器/写入器也没有被指定为线程安全的,但它们确实公开了子类的锁对象。除了固定之外,这些类中的同步是有问题的和不一致的;例如,InputStreamReader和OutputStreamWriter使用的流解码器和编码器在流对象上同步,而不是在锁对象上同步。为防止固定,实现现在的工作方式如下:BufferedInputStream、BufferedOutputStream、BufferedReader、BufferedWriter、PrintStream和PrintWriter现在在直接使用时使用显式锁而不是监视器。这些类在子类时与以前一样同步。InputStreamReader和OutputStreamWriter使用的流解码器和编码器现在使用与封闭的InputStreamReader或OutputStreamWriter相同的锁。更进一步,消除所有这些通常不必要的锁定超出了本JEP的范围。此外,BufferedOutputStream、BufferedWriter和OutputStreamWriter的流编码器使用的缓冲区的初始大小现在更小,以便在堆中有许多流或写入程序时减少内存使用-如果有是一百万个虚拟线程,每个线程在套接字连接上都有一个缓冲流。Java Native Interface (JNI)JNI定义了一个新函数IsVirtualThread,用于测试对象是否为虚拟线程。JNI规范在其他方面保持不变。调试调试体系结构由三个接口组成:JVM Tool Interface (JVM TI)、Java Debug Wire Protocol (JDWP)和Java Debug Interface (JDI)。现在,所有三个接口都支持虚拟线程。更新到JVM TI是:大多数用jthread 调用的函数(即,对线程对象的JNI引用)都可以用对虚拟线程的引用调用。虚拟线程不支持少量函数,即PopFrame、ForceEarlyReturn、StopThread、AgentStartFunction和GetThreadCpuTime。SetLocal*函数仅限于在断点或单步事件挂起的虚拟线程的最顶层帧中设置局部变量。现在,GetAllThreads和GetAllStackTraces函数被指定为返回所有平台线程,而不是所有线程。除在早期VM启动或堆迭代期间发布的事件外,所有事件都可以在虚拟线程的上下文中调用事件回调。挂起/恢复实现允许调试器挂起和恢复虚拟线程,并允许在挂起虚拟线程时挂起载体线程。一项新功能can_support_virtual_threads,使代理可以更精细地控制虚拟线程的线程开始和结束事件。新函数支持虚拟线程的批量挂起和恢复;这些函数需要can_support_virtual_threads功能。现有的JVM TI代理将与以前一样工作,但如果它们调用虚拟线程不支持的函数,则可能会遇到错误。当不知道虚拟线程的代理与使用虚拟线程的应用程序一起使用时,就会出现这些情况。对某些代理来说,更改GetAllThreads以返回仅包含平台线程的数组可能是一个问题。启用ThreadStart和ThreadEnd事件的现有代理可能会遇到性能问题,因为它们无法将这些事件限制在平台线程上。更新到JDWP是:一个新命令允许调试器测试线程是否为虚拟线程。EventRequest命令上的新修饰符允许调试器将线程开始和结束事件限制在平台线程上。更新到JDI是:一种新的方法com.sun.jdi.ThreadReference测试线程是否为虚拟线程。中的新方法com.sun.jdi.request.ThreadStartRequest和com.sun.jdi.request.ThreadDeathRequest将为请求生成的事件限制为平台线程。如上所述,虚拟线程不被视为线程组中的活动线程。因此,JVM TI函数GetThreadGroupCalt、JDWP命令ThreadGroupReference/Children和JDI方法 com.sun.jdi.ThreadGroupReference.threads() 返回的线程列表仅包括平台线程。JDK Flight Recorder (JFR)JFR支持具有多个新事件的虚拟线程:jdk.VirtualThreadStart和jdk.VirtualThreadEnd表示虚拟线程的开始和结束。默认情况下,这些事件是禁用的。jdk.VirtualThreadPinned表示虚拟线程在被固定时被驻留,即,没有释放其载体线程(参见限制)。此事件默认启用,阈值为20ms。jdk.VirtualThreadSubmitFailed表示启动或取消驻留虚拟线程失败,可能是由于资源问题。默认情况下,此事件处于启用状态。Java Management Extensions (JMX)一种新的方法com.sun.management.HotSpotDiagnosticsMXBean 生成描述的新式线程转储以上。该方法也可以通过平台间接调用 MBeanServer从本地或远程JMX工具。java.lang.management.ThreadMXBean 仅支持平台线程的监控和管理。java.lang.ThreadGroupjava.lang.ThreadGroup是一个用于分组线程的旧API,在现代应用程序中很少使用,不适合分组虚拟线程。我们现在不推荐并降级它,并希望在未来引入一个新的线程组织结构,作为结构化并发。作为背景,ThreadGroup API可以追溯到Java 1.0。它最初旨在提供作业控制操作,如停止组中的所有线程。现代代码更倾向于使用java.util.concurrent的线程池API(在Java 5中引入)。ThreadGroup在早期Java版本中支持小程序的隔离,但Java安全体系结构在Java 1.2中发生了显著的发展,ThreadGroup不再发挥重要作用。ThreadGroup也旨在用于诊断目的,但该角色已被Java 5中引入的监控和管理功能所取代,包括java.lang.管理API.除了现在基本上不相关之外,ThreadGroup API和实现还存在许多重大问题:销毁线程组的API和机制存在缺陷。API要求实现引用组中的所有活动线程。这将为线程创建、线程启动和线程终止增加同步和竞争开销。API定义了 enumerate() 方法,这些方法本身是有效的。API定义了suspend()、resume()和stop()方法,这些方法本身就容易死锁和不安全。现在指定、不建议使用和降级ThreadGroup ,如下所示:删除了显式销毁线程组的能力:最终不建议使用的destroy()方法什么都不做。守护程序线程组的概念已删除:守护程序状态设置并由最终弃用的setDaemon(boolean) 和 isDaemon() 方法被忽略。该实现不再保留对子组的强引用。当线程组中没有活动线程,并且没有其他任何其他线程保持线程组活动时,线程组现在有资格被垃圾收集。最终不建议使用的suspend()、resume()和stop()方法总是抛出异常。参考引用原文同步至:https://waylau.com/jep-425-virtual-threads-preview更多Java最新特性可见柳伟卫所著《Java核心编程》:https://github.com/waylau/modern-java-demoshttps://openjdk.java.net/jeps/425
JDK 18概述JDK 18是Java SE平台版本18的开源参考实现,如JSR 393在Java社区进程中。 JDK 18在2022年3月22日正式发布。特性400: 默认UTF-8408: 简单Web服务器413: Java API文档中的代码段416: 使用方法句柄重新实现核心反射417: Vector API(第三个孵化器)418: 互联网地址解析SPI419: 外部函数和内存API(第二个孵化器)420: switch的模式匹配(第二次预览)421: 不建议删除最终确定JEP 400:默认为UTF-8总结指定UTF-8作为标准Java API的默认字符集。通过此更改,依赖于默认字符集的API将在所有实现、操作系统、区域设置和配置中一致地行为。目标当Java程序的代码依赖于默认字符集时,使其更可预测和可移植。澄清标准Java API使用默认字符集的位置。在整个标准Java API中标准化UTF-8,控制台I/O除外。 非目标定义新的标准Java API或受支持的JDK API并不是目标,尽管这项努力可能会发现新的便利方法可能会使现有API更平易近人或更易于使用的机会。不建议使用或删除依赖于默认字符集而不是采用显式字符集参数的标准Java API。 动机用于读取和写入文件以及处理文本的标准Java API允许将字符集作为参数传递。字符集控制Java编程语言的原始字节和16位字符值之间的转换。支持的字符集包括,例如,US-ASCII、UTF-8和ISO-8859-1。 如果未传递字符集参数,则标准Java API通常使用默认字符集。JDK在启动时根据运行时环境选择默认字符集:操作系统、用户的区域设置和其他因素。 由于默认字符集在任何地方都不一样,使用默认字符集的API会带来许多不明显的危险,即使对有经验的开发人员也是如此。 考虑一个应用程序,它创建了java.io.FileWriter,而不传递字符集,然后使用它向文件写入一些文本。生成的文件将包含使用运行应用程序的JDK的默认字符集编码的字节序列。第二个应用程序在不同的计算机上运行,或由同一计算机上的不同用户运行,在不传递字符集的情况下创建java.io.FileReader,并使用它读取该文件中的字节。生成的文本包含使用运行第二个应用程序的JDK的默认字符集解码的字符序列。如果第一个应用程序的JDK和第二个应用程序的JDK之间的默认字符集不同,则生成的文本可能会被静默损坏或不完整,因为FileReader无法判断它使用了相对于FileWriter的错误字符集解码文本。以下是这种危险的一个示例,在macOS上以UTF-8编码的日语文本文件在Windows上以美国英语或日语语言环境读取时已损坏:java.io.FileReader(“hello.txt”) -> “こんにちは” (macOS)java.io.FileReader(“hello.txt”) -> “ã?“ã‚“ã?«ã?¡ã? ” (Windows (en-US))java.io.FileReader(“hello.txt”) -> “縺ォ縺。縺ッ” (Windows (ja-JP)熟悉此类危害的开发人员可以使用显式接受字符集参数的方法和构造函数。但是,必须传递参数可以防止方法和构造函数通过流管道中的方法引用(::)使用。 开发人员有时会尝试通过在命令行上设置系统属性file.encode(即java-Dfile.encode=...)来配置默认字符集,但这从未得到支持。此外,在Java运行时启动后,尝试以编程方式设置属性(即System.setProperty(...))是不起作用的。 并非所有标准Java API都服从JDK的默认字符集选择。例如,java.nio.file.Files中读取或写入没有Charset参数的文件的方法被指定为始终使用UTF-8。较新的API默认使用UTF-8,而较旧的API默认使用默认字符集,这对于使用混合API的应用程序来说是一个危险。 如果默认字符集在任何地方都被指定为相同,整个Java生态系统都将受益。不关心可移植性的应用程序将不会受到什么影响,而通过传递字符集参数来支持可移植性的应用程序将不会受到影响。UTF-8有长期以来一直是万维网上最常见的字符集,UTF-8是由大量Java程序处理的XML和JSON文件的标准,Java自己的API越来越青睐UTF-8,例如NIO接口以及属性文件因此,将UTF-8指定为所有Java API的默认字符集是有意义的。 我们认识到,此更改可能会对迁移到JDK 18的程序产生广泛的兼容性影响。因此,始终可以恢复JDK 18之前的行为,其中默认字符集取决于环境。说明在JDK 17和更早版本中,默认字符集是在Java运行时启动时确定的。在macOS上,它是UTF-8,POSIX C区域设置除外。在其他操作系统上,它取决于用户的区域设置和默认编码,例如,在Windows上,它是基于代码页的字符集,如windows-1252或windows-31j。方法java.nio.charsets.Charset.defaultCharset()返回默认字符集。查看当前JDK默认字符集的快速方法是使用以下命令:java -XshowSettings:properties -version 2>&1 | grep file.encoding几个标准Java API使用默认字符集,包括: 在http://java.io包中,InputStreamReader、FileReader、OutputStreamWriter、FileWriter和PrintStream定义构造函数,以创建使用默认字符集编码或解码的读取器、写入器和打印流。 在java.util包中,Formatter和Scanner定义了构造函数,其结果使用默认字符集。 在http://java.net包中,URLEncoder和URLDecoder定义了使用默认字符集的过时方法。 我们建议更改Charset.defaultCharset()的规范,以说明默认字符集是UTF-8除非通过特定于实现的手段另有配置。(有关如何配置JDK,请参见下文。)UTF-8字符集由RFC 2279;它所依据的转换格式在ISO 10646-1修正案2中规定,并在Unicode标准.不要与修改后的UTF-8. 我们将更新所有使用默认字符集交叉引用Charset.defaultCharset()的标准Java API的规范。这些API包括上面列出的API,但不包括System.out和System.err,它们的字符集将由 Console.charset()定义。file.encoding和native.encoding系统属性正如Charset.defaultCharset()规范所设想的那样,JDK将允许将默认字符集配置为UTF-8以外的其他东西。我们将修改系统属性file.encoding的处理,以便在命令行上设置它是配置默认字符集的支持方法。我们将在实施说明中具体说明这一点 System.getProperties()如下: 如果file.encoding设置为“COMPAT”(即java -Dfile.encoding=COMPAT),则默认字符集将是JDK 17及更早版本中算法选择的字符集,基于用户的操作系统、区域设置和其他因素。file.encode的值将设置为该字符集的名称。 如果file.encoding设置为“UTF-8”(即java -Dfile.encoding=UTF-8),则默认字符集将为UTF-8。定义此no-op值是为了保留现有命令行的行为。 未指定“Compat”和“UTF-8”以外的值的处理。它们不受支持,但如果这样的值在JDK 17中有效,那么它很可能会在JDK 18中继续有效。JDK 17引入了native.encoding 系统属性作为程序获取JDK算法选择的字符集的标准方式,无论默认字符集是否实际配置为该字符集。在JDK 18中,如果在命令行上将file.encoding设置为COMPAT,则file.encoding的运行时值将与 native.encoding的运行时值相同;如果在命令行上将file.encoding设置为UTF-8,则file.encoding的运行时值可能与native.encoding的运行时值不同。 在下面的风险和假设中,我们讨论了如何减轻此更改对file.encoding可能产生的不兼容问题,以及native.encoding系统属性和应用程序建议。 JDK内部使用了三个与字符集相关的系统属性。它们仍然未指明和不支持,但为了完整性,请在此处记录: sun.stdout.encode和sun.stderr.encode-标准输出流(System.out)和标准错误流(System.err)以及java.io.Console API中使用的字符集的名称。 sun.jnu.encode-java.nio.file实现在编码或解码文件名路径时使用的字符集的名称,而不是文件内容。在macOS上,它的值是“UTF-8”;在其他平台上,它通常是默认字符集。源文件编码Java语言允许源代码在UTF-16编码,这不受默认字符集选择UTF-8的影响。但是,javac编译器会受到影响,因为它假定.java源文件是使用默认字符集编码的,除非-encoding 另有配置选项。如果源文件是用非UTF-8编码保存的,并使用较早的JDK编译的,则在JDK 18或更高版本上重新编译可能会导致问题。例如,如果非UTF-8源文件具有包含非ASCII字符的字符串文字,则除非使用-encoding ,否则JDK 18或更高版本中的javac可能会误解这些文字。旧版默认字符集在JDK 17和更早版本中,名称默认值被识别为US-ASCII字符集的别名。也就是说,Charset.forName("default")产生的结果与Charset.forName("US-ASCII")相同。默认别名是在JDK 1.5中引入的,以确保使用http://sun.io converters的旧代码可以迁移到JDK 1.4中引入的java.nio.charset框架。 当默认字符集指定为UTF-8时,JDK 18将默认值保留为US-ASCII的别名将非常混乱。当用户通过在命令行上设置 -Dfile.encoding=COMPAT将默认字符集配置为其JDK 18之前的值时,默认值的含义也会令人困惑。将默认值重新定义为不是US-ASCII的别名,而是默认字符集(无论是UTF-8还是用户配置的)的别名,将导致调用 Charset.forName("default")的(少数)程序中的微妙行为更改。 我们认为,继续承认JDK 18的违约将延长一个糟糕的决定。它不是由Java SE平台定义的,也不是由IANA识别为任何字符集的名称或别名。事实上,对于基于ASCII的网络协议,IANA鼓励使用规范名称US-ASCII,而不仅仅是ASCII或模糊别名,如ANSI_X3.4-1968,使用特定于JDK的别名默认值与该建议背道而驰。Java程序可以使用枚举常量StandardCharsets.US_ASCII来明确其意图,而不是将字符串传递给Charset.forName(...)。 因此,在JDK 18中,Charset.forName("default")将引发不支持的UnsupportedCharsetException。这将使开发人员有机会检测使用该成语,并迁移到US-ASCII或Charset.defaultCharset()的结果。替代方案 保持现状-这并不能消除上述危险。 不建议在Java API中使用默认字符集的所有方法-这将鼓励开发人员使用采用字符集参数的构造函数和方法,但生成的代码将更加详细。 指定UTF-8作为默认字符集,而不提供任何更改它的方法-此更改的兼容性影响将太高。参考引用更多示例可见《Java核心编程》:GitHub - waylau/modern-java-demos: Modern Java: From Java 8 to Java 14. 现代Java案例大全/《Java核心编程》源码
本文是对《Vue.js 3企业级应用开发实战》这本书做了拆箱和导读。介绍本书的写作背景、特点等。整个拆书过程的视频也可见B站:https://www.bilibili.com/video/BV1SL411w7ke/封面部分首先是介绍封面部分。《Vue.js 3企业级应用开发实战》封面部分是采用了Vue的LOGO作为的整体的设计风格。具有非常高的辨识度。可以看到,是由电子工业出版社博文视点出品。右下角可以看到该书的四个特点。理论落实到代码实现买南非提供实例源码知识面广、适合新手商业性、应用性强封底部分介绍封底部分。封底部分可以看到是由电子工业出版社博文视点出品的其他书籍的介绍。这本书归类为前端开发Vue.js。定价为109元。勒口部分勒口部分是作者的简介。一句话概括:经验丰富、著作等身。内容简介本书基于最新的Vue.js 3技术栈展开,全面讲解了Vue.js 3在企业级领域的应用。本书也是一本面向Vue.js 3的“大而全”的教程,力图涉及Vue.js 3的所有知识点,包括应用实例、组件、模板、计算属性、监听器、指令、表单、事件、数据绑定、路由、依赖注入、自定义样式、动画、渲染函数、测试、响应式编程等方面内容,同时还介绍了Vue CLI、TypeScript、Animate.css、Mocha、Vue Router、Naive UI、vue-axios等Vue.js周边生态方面的内容。特别是 Vue.js 3完全支持TypeScript,使编程更加强调采用类、面向对象的方式去组织代码。最后,本书会手把手带领读者一起从零开始实现一个完整的企业级新闻头条客户端应用。本书技术前瞻、面向实战、案例丰富。本书主要面向的是对Vue.js感兴趣的学生、前端工程师、系统架构师。该书是2022年1月份出版的。写作背景Vue.js是近些年广受关注的前端框架。Vue.js已经具备了商业项目开发的必备条件,如语法精炼、优雅而简洁、代码的可读性高、成熟的组件模块化等等,当然,还有商业项目开发最为看重的与第三方控件的结合能力。正是这些能力,确保了“后浪”Vue.js能够与React、Angular等老牌前端开发框架并驾齐驱,在开发者当中占据越来越重要的位置。和React、Angular相比,Vue.js在可读性、可维护性和趣味性之间做到了很好的平衡,非常使用中小型项目的快速开发。随着Vue.js 3的推出,Vue.js具备了进军大型项目开发的可能性。市面上关于Vue.js 1.x和2.x版本的介绍资料比较多,但由于Vue.js 3是近期才发布,因此,市面上有关ue.js 3的资料比较匮乏。于是,笔者在GitHub上,以开源方式撰写了《跟老卫学Vue.js开发》开源书[1],介绍Vue.js 3的使用。同时,考虑到能让更多的人学习到Vue.js,于是笔者以该开源书为蓝本,对Vue.js 3的知识点做了完整的梳理和扩展,补充了实战案例,出版了《Vue.js 3企业级应用开发实战》一书以补空白。希望读者通过本书的学习,掌握Vue.js 3企业级应用开发实战的能力。内容介绍本书大致分为以下几个部分:准备(第1-2章):了解Vue.js基础概念,并带领读者快速创建一个Vue.js应用,使读者对Vue.js有一个初步的印象。入门(第3-11章):了解TypeScript基础、Vue.js应用实例、组件基础、模板、计算属性与监听器、样式、表达式、事件、表单等概念。通过这几章的学习,读者可以了解到 Vue.js常用的知识点。进阶(第12-18章):深入讲解Vue.js高级知识点。实战(第19-22章):手把手带领读者一起从零开始实现一个完整的企业级新闻头条客户端应用,使读者具备Vue.js企业级应用开发的完整的能力。本书特点1.可与笔者在线上交流本书提供线上交流网址:https://github.com/waylau/vuejs-enterprise-application-development/issues读者有任何技术上的问题,都可以向笔者提问。2.提供了基于知识点的77实例和6个综合性实战案例本书提供了丰富的基于Vue.js 3技术点的实例77个,将理论讲解最终落实到代码实现上来。在掌握了基础之后,另外提供了6个综合性实战案例。这些案例从零开始,最终实现了一个完整的企业级应用,内容具有很高的应用价值和参考性。3.免费提供书中实例的源文件本书提供了书中涉及的所有实例的源文件。读者可以一边阅读本书,一边参照源文件动手练习,这样不仅提高了学习的效率,而且可以对书中的内容有更加直观的认识,从而逐渐培养自己的编程能力。4.覆盖的知识面广本书几乎囊括了Vue.js 3所涉及的包括应用实例、组件、模板、计算属性、监听器、指令、表单、事件、数据绑定、路由、依赖注入、自定义样式、动画、渲染函数、测试、响应式编程等方面内容,同时介绍了Vue CLI、TypeScript、Animate.css、Mocha、Vue Router、Naive UI、vue-axios等Vue.js周边生态方面的内容,技术前瞻,案例丰富。不管是编程初学者,还是编程高手,都能从本书中获益。本书可作为读者案头的工具书,随手翻阅。5.采用短段、短句,便于流畅阅读本书采用结构化的层次,并采用短小的段落和语句,让读者读来有顺水行舟的轻快感。6.案例的商业性、应用性强本书提供的案例多数来源于真正的商业项目,具有高度的参考价值。有些代码甚至可以直接移植到自己的项目中,进行重复使用,使从“学”到“用”这个过程变得更加直接。7.印刷精良其实除了上述列出的6点外,我认为还有其他特点,就是这本书的印刷。比如这本书本身是双色版的,黑色和绿色,方便辨识正文和标题、代码和注释。还有一点,这本书的纸张是米黄色的,非常的护眼。对于爱学习的你来说,可以更好的保护自己的哦。源代码本书提供的素材和源代码可从以下网址下载:https://github.com/waylau/vuejs-enterprise-application-development勘误和交流本书如有勘误,会在以下网址发布:https://github.com/waylau/vuejs-enterprise-application-development/issues读者通过以下方式与笔者联系。博客:https://waylau.com邮箱:waylau521@gmail.com微博:http://weibo.com/waylau521GitHub:https://github.com/waylau
随着应用规模的增长,越来越多的开发者认识到静态语言的好处。静态类型系统可以帮助防止许多潜在的运行时错误,这就是为什么Vue 3是用TypeScript编写的。这意味着在Vue应用开发中,使用TypeScript进行开发不需要任何其他工具。TypeScript在Vue 3世界中是一等公民。有两种方式,可以实现在Vue 3应用中支持使用TypeScript。1. 基于Vue 3 Preview创建的项目如果是选择Vue 3模板“Vue 3 Preview”进行项目创建,正如前文hello-world应用那样,则可以采用如下的步骤实现对TypeScript的支持。在应用的根目录下执行如下命令:vue add typescript此时,在命令行会出现提示框,根据提示选择即可。这里都是选“Y”即可,看到如下输出内容,则证明已经完成。2. Manually select features(手动选择)方式如果是采用Manually select features(手动选择)方式创建应用,则直接可以选择TypeScript作为支持选项。具体步骤如下。选择“TypeScript”,而后回车。选择“3.x (Preview)”,而后回车。 3. TypeScript应用的差异相比于JavaScript的应用而言,TypeScript的应用的目录结构如下。 l 多了TypeScript语言的配置文件tsconfig.jsonl package.json和package-lock.json中多了对TypeScript等依赖的描述l main.js改为了main.tsl 多了shims-vue.d.ts文件l 所有在Vue组件中使用JavaScript的地方,都改为了TypeScript参考引用本系列归档至《跟老卫学Vue.js开发》:GitHub - waylau/vuejs-enterprise-application-development: Vue.js Enterprise Application Development. 《跟老卫学Vue.js开发》/《Vue.js 3企业级应用开发实战》源码参考书籍《Vue.js 3企业级应用开发实战》:《Vue.js 3企业级应用开发实战(双色版)(博文视点出品)》(柳伟卫)【摘要 书评 试读】- 京东图书
很多人对鸿蒙、OpenHarmony、HarmonyOS这些术语傻傻的分不清楚,那么本文就做一些解答。6月4日,华为发布了《关于规范HarmonyOS沟通口径的通知》(以下简称《通知》),原文在网上都能搜到,这里就不贴了。本人对该通知做一些解读如下。1. 《通知》背景《通知》开篇就说“关于‘鸿蒙操作系统’,由于缺乏规范的表述和统一的口径,导致内部理解不一致、对外说法不一致,容易引起混淆”所以,大家不要不好意思啊,不光你不懂,很多人都傻傻的分不清。不懂不要怕,咱们继续往下看。2. 鸿蒙操作系统“鸿蒙操作系统”特指华为智能终端操作系统。“鸿蒙操作系统”具有以下特征:一是一套操作系统可以满足大大小小设备需求,实现统一OS,弹性部署;二是搭载该操作系统的设备在系统层面融为一体、形成超级终端,让设备的硬件能力可以弹性扩展,实现设备之间硬件互助,资源共享;三是面向开发者,实现一次开发,多端部署。3. OpenHarmonyOpenHarmony是一个开源项目,由开放原子开源基金会(https://www.openatom.org/)进行管理。开放原子开源基金会由华为、阿里、腾讯、百度、浪潮、招商银行、360等十家互联网企业共同发起组建。OpenHarmony暂时还没有中文名字,名字还在申请中。项目地址为:https://gitee.com/openharmonyOpenHarmony开源项目主要包括两部分:一是华为捐献的“鸿蒙操作系统”的基础能力;二是其他参与者的贡献。因此,OpenHarmony是“鸿蒙操作系统”的底座。4. HarmonyOSHarmonyOS就是“鸿蒙操作系统”,或者简称为“鸿蒙OS”是基于 OpenHarmony、AOSP等开源项目的商用版本。这里需要注意:一是HarmonyOS不是开源项目,而是商用版本。二是HarmonyOS手机和平板之所以能运行Android,是因为HarmonyOS 实现了现有Android生态应用(即AOSP)的运行。5. 鸿蒙生态鸿蒙生态包括 OpenHarmony 和 HarmonyOS,当然还包括开发工具以周边的一些开发库。当我们在说“鸿蒙”的时候,也许就是指鸿蒙生态。6. 何时选择OpenHarmony或是HarmonyOS?如果你只是一个应用开发工程师,专注于终端设备的应用开发,那么选择HarmonyOS。这里也有免费开源教程《跟老卫学HarmonyOS开发》https://github.com/waylau/harmonyos-tutorial如果你对HarmonyOS底层的技术感兴趣,想了解或者想对HarmonyOS做贡献,那么选择OpenHarmony。当然,如果想更进一步,做一款属于自己的操作系统,基于OpenHarmony开源项目做二次开发也是不错的选择哦。以上就是本人对于鸿蒙相关术语的一些解读,看我理解的对不对呢?欢迎大家讨论。
《Vue.js 3企业级应用开发实战》一书由电子工业出版社出版,已经于2022年1月出版上市。近日拿到了样书,第一时间希望与读者朋友们分享下这本书里面的内容。这本书的背景近期拿到了样书,迫不及待的对新书做了浏览。同时也做了拆书与导读。聊下为啥要写这本书。Vue.js是近些年广受关注的前端框架。Vue.js已经具备了商业项目开发的必备条件,如语法精炼、优雅而简洁、代码的可读性高、成熟的组件模块化等等,当然,还有商业项目开发最为看重的与第三方控件的结合能力。正是这些能力,确保了“后浪”Vue.js能够与React、Angular等老牌前端开发框架并驾齐驱,在开发者当中占据越来越重要的位置。和React、Angular相比,Vue.js在可读性、可维护性和趣味性之间做到了很好的平衡,非常使用中小型项目的快速开发。随着Vue.js 3的推出,Vue.js具备了进军大型项目开发的可能性。市面上关于Vue.js 1.x和2.x版本的介绍资料比较多,但由于Vue.js 3是近期才发布,因此,市面上有关ue.js 3的资料比较匮乏。于是,笔者在GitHub上,以开源方式撰写了《跟老卫学Vue.js开发》开源书(https://github.com/waylau/vuejs-enterprise-application-development),介绍Vue.js 3的使用。同时,考虑到能让更多的人学习到Vue.js,于是笔者以该开源书为蓝本,对Vue.js 3的知识点做了完整的梳理和扩展,补充了实战案例,出版了《Vue.js 3企业级应用开发实战》一书以补空白。希望读者通过本书的学习,掌握Vue.js 3企业级应用开发实战的能力。这本书的内容本书大致分为以下几个部分:准备(第1-2章):了解Vue.js基础概念,并带领读者快速创建一个Vue.js应用,使读者对Vue.js有一个初步的印象。入门(第3-11章):了解TypeScript基础、Vue.js应用实例、组件基础、模板、计算属性与监听器、样式、表达式、事件、表单等概念。通过这几章的学习,读者可以了解到 Vue.js常用的知识点。进阶(第12-18章):深入讲解Vue.js高级知识点。实战(第19-22章):手把手带领读者一起从零开始实现一个完整的企业级新闻头条客户端应用,使读者具备Vue.js企业级应用开发的完整的能力。这本书的特点概况起来,这本《Vue.js 3企业级应用开发实战》主要有以下几个特点。1.可与笔者在线上交流本书提供线上交流网址:https://github.com/waylau/vuejs-enterprise-application-development/issues读者有任何技术上的问题,都可以向笔者提问。2.提供了基于知识点的77实例和6个综合性实战案例本书提供了丰富的基于Vue.js 3技术点的实例77个,将理论讲解最终落实到代码实现上来。在掌握了基础之后,另外提供了6个综合性实战案例。这些案例从零开始,最终实现了一个完整的企业级应用,内容具有很高的应用价值和参考性。3.免费提供书中实例的源文件本书提供了书中涉及的所有实例的源文件。读者可以一边阅读本书,一边参照源文件动手练习,这样不仅提高了学习的效率,而且可以对书中的内容有更加直观的认识,从而逐渐培养自己的编程能力。源码见:https://github.com/waylau/vuejs-enterprise-application-development/4.覆盖的知识面广本书几乎囊括了Vue.js 3所涉及的包括应用实例、组件、模板、计算属性、监听器、指令、表单、事件、数据绑定、路由、依赖注入、自定义样式、动画、渲染函数、测试、响应式编程等方面内容,同时介绍了Vue CLI、TypeScript、Animate.css、Mocha、Vue Router、Naive UI、vue-axios等Vue.js周边生态方面的内容,技术前瞻,案例丰富。不管是编程初学者,还是编程高手,都能从本书中获益。本书可作为读者案头的工具书,随手翻阅。5.采用短段、短句,便于流畅阅读本书采用结构化的层次,并采用短小的段落和语句,让读者读来有顺水行舟的轻快感。6.案例的商业性、应用性强本书提供的案例多数来源于真正的商业项目,具有高度的参考价值。有些代码甚至可以直接移植到自己的项目中,进行重复使用,使从“学”到“用”这个过程变得更加直接。参考引用原本同步至:https://waylau.com/vuejs-enterprise-application-development/京东有售:https://item.jd.com/13624356.html当当有售:http://product.dangdang.com/29373978.html
1. 安装Node.js2. 安装Vue CLI执行如下命令npm install -g @vue/cli看到如下输出,则证明安装成功3. 创建项目vue create hello-world选择Vue 3模板“Vue 3 Preview”,并回车。看到如下内容,则证明hello-world项目已经创建完成。4. 运行项目当前目录下,执行cd hello-worldnpm run serve看到如下输出,则证明项目已经启动。访问http://localhost:8080/可以看到项目的主页,如下。至此,已经完成了第一个Vue 3应用的创建!参考引用本系列归档至《跟老卫学Vue.js开发》:https://github.com/waylau/vuejs-enterprise-application-development参考书籍《Vue.js 3企业级应用开发实战》:https://item.jd.com/13624356.html
《鸿蒙HarmonyOS手机应用开发实战》一书由清华大学出版社出版,已经于2022年1月上市。拿到了样书,第一时间希望与读者朋友们分享下这本书里面的内容。这本书的背景近期拿到了样书,迫不及待的对新书做了浏览。同时也做了拆书与导读,可以在B站找到:https://www.bilibili.com/video/BV1gL411c7hg/聊下为啥要写这本书。中国信息产业一直是“缺芯少魂”,其中的“芯”指的是芯片,而“魂”则是指操作系统。而自2019年5月15日起,美国陆续把包括华为在内的中国高科技企业列入其所谓的“实体清单”(Entities List),标志着科技再次成为中美博弈的核心领域。随着谷歌暂停与华为的部分合作,包括软件和技术服务的转让,华为在国外市场已经面临着升级Android版本、搭载谷歌服务等方面的困境。在这样的背景下,华为顺势推出HarmonyOS,以求在操作系统领域不受制于人。HarmonyOS是一款面向未来、面向全场景(移动办公、运动健康、社交通信、媒体娱乐等)的全新的分布式操作系统。作为操作系统领域的新成员,HarmonyOS势必会面临bug多、学习资源缺乏等众多困难。为此,笔者在开源社区以开源方式推出了免费系列学习教程《跟老卫学HarmonyOS开发》 ,以帮助HarmonyOS爱好者入门。同时,为了让更多的人了解并使用HarmonyOS,笔者将自身工作、学习中遇到的问题、难题进行了总结,形成了本书,以补市场空白。几个特点概况起来,这本《鸿蒙HarmonyOS手机应用开发实战》主要有三大特点。B站也有相关介绍:https://www.bilibili.com/video/BV1gL411c7hg/1、涉及面非常广那么涉及广的话可以体现在哪里呢?可以看这本书的内容简介部分。本书采用新的HarmonyOS 2版本作为基石,详细介绍如何基于HarmonyOS进行手机应用的开发,内容涵盖HarmonyOS架构、DevEco Studio、应用结构、Ability、任务调度、公共事件、通知、剪切板、Java UI、JS UI、多模输入、线程管理、视频、图像、相机、音频、媒体会话管理、媒体数据管理、安全管理、二维码、通用文字识别、蓝牙、WLAN、网络管理、电话服务、设备管理、数据管理、原子化服务、流转等多个主题。本书辅以大量解决实际问题的案例,具有很强的前瞻性、应用性、趣味性。可以说,HarmonyOS常见的知识点这本书都已经涉及了,毕竟713页的篇幅不是盖的。更难能可贵的是,这本书也对当前比较新的特新诸如原子化服务、流转也做了讲解。2、图例非常丰富这本书是他这个图例非常丰富,从基本的IDE安装,到复杂的数据结构的演示,都有丰富的图例。通过图例的配套讲解演示,可以方便读者理解。3、详细的接口说明每个用到的API的用法都给你描述的详详细细的。4、代码量非常大第三个特点的话就是它里面的实战案例非常丰富。实战案例体现在,这本书的每一章每个知识点基本上会配套一个实战案例,代码量是非常大的。每行代码这个重点代码它都有一些注释给你写得明明白白。这本书呢是不单只是简单的讲一些理论,它还有会手把手的教你写代码。理论联系实际。5、本书开源本书也是有配套的开源书籍,如果不想花钱,也是有免费版本的哦,见《跟老卫学HarmonyOS开发》https://github.com/waylau/harmonyos-tutorial学习本书,一起构建万物互联的时代!参考引用原本同步至:https://waylau.com/about-harmonyos-mobile-application-development-book/京东有售:https://item.jd.com/13568130.html————————————————版权声明:本文为CSDN博主「_waylau」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。原文链接:https://blog.csdn.net/kkkloveyou/article/details/122484759
本节是《Java数据结构及算法实战》系列的第2节,主要介绍描述算法的常用的4种方式。要定义一个算法,我们可以用自然语言、流程图、伪代码的方式描述解决某个问题的过程或是编写一段程序来实现这个过程。比如,在前面所举的“学生信息管理系统”例子中,我们希望实现添加用户、删除用户、查询用户三个算法。1. 自然语言描述算法可以采用自然语言的方式来描述添加用户、删除用户、查询用户三个算法:添加用户:将用户信息添加到系统中。如果已经添加了过该用户的信息,则提示用户。否则将用户信息添加到系统中,并给出提示。删除用户:将用户信息从系统中删除。如果用户信息不存在于系统中,则提示用户。否则将用户信息从系统中删除,并给出提示。查询用户:将系统中所有的用户信息查询出来。如果系统中不存在用户,则提示用户。否则将用户信息查询出来返回,并将用户信息打印出来。使用自然语言描述的好处是任何人都能看懂。当然相比于伪代码或者程序语言而言,使用自然语言描述有时会显得繁琐。2. 流程图描述算法流程图(Flow Diagram)是一种通用的图形符号表示法是一种非正式的,可以清楚描述步骤和判断。图1-2展示的是用流程图的方式来描述添加用户、删除用户、查询用户三个算法。相比较自然语言而言,通过流程图的描述,可以很清楚的看到操作的流向及经过的步骤。但需要注意的是,流程图应该只描述核心的操作步骤以及关键的节点判断,而不是事无巨细的把所有的操作都描述出来,否则只会让整个图看上去复杂难以理解。3. 伪代码描述算法伪代码(Pseudocode)是一种非正式的,类似于英语结构的,用于描述模块结构图的语言。可以采用伪代码的方式来描述添加用户、删除用户、查询用户三个算法。添加用户的伪代码如下:input(student) if student in studentList print "Student exsit" else add student in studentList print "Add student success"删除用户的伪代码如下:input(student) if student in studentList remove student from studentList print "Remove student success" else print "Student not exsit"查询用户的伪代码如下:if student in studentList output studentList else print "No student exsit"伪代码结构清晰、代码简单、可读性好,并且类似自然语言。介于自然语言与编程语言之间。以编程语言的书写形式指明算法职能。使用伪代码,不用拘泥于具体实现。相比程序语言(例如Java、C++、C等等)它更类似自然语言。它虽然不是标准的语言,却可以将整个算法运行过程的结构用接近自然语言的形式(可以使用任何一种你熟悉的文字,关键是把程序的意思表达出来)描述出来。4. 程序语言描述算法程序语言描述算法,实际上就是用程序语言实现算法。不同的编程语言其语法不尽相同。以下是采用Java语言的方式来描述添加用户、删除用户、查询用户三个算法。import java.util.ArrayList; import java.util.List; public class StudentInfoManageSystem { private List<Student> studentList = new ArrayList<>(); public void addStudent(Student student) { // 如果已经添加了过该用户的信息,则提示用户。 // 否则将用户信息添加到系统中,并给出提示。 if (studentList.contains(student)) { System.out.println("Student exsit"); } else { studentList.add(student); System.out.println("Add student success"); } } public void removeStudent(Student student) { // 如果用户信息不存在于系统中,则提示用户。 // 否则将用户信息从系统中删除,并给出提示。 if (studentList.contains(student)) { studentList.remove(student); System.out.println("Remove student success"); } else { System.out.println("Student not exsit"); } } public List<Student> getStudentList() { // 如果系统中不存在用户,则提示用户。 // 否则将用户信息查询出来返回,并将用户信息打印出来。 if (studentList.isEmpty()) { System.out.println("No student exsit"); } else { for (Student s : studentList) { System.out.format("Student info: name %s, age %d, phone %s, address %s%n", s.getName(), s.getAge(), s.getPhoneNumer(), s.getAddress()); } } return studentList; } }为了演示上述算法,还需要一个应用入口。我们用StudentInfoManageSystemDemo类来表示应用主程序,代码如下:import java.util.ArrayList; import java.util.List; public class StudentInfoManageSystemDemo { public static void main(String[] args) { // 初始化系统 StudentInfoManageSystem system = new StudentInfoManageSystem(); // 初始化学生信息 Student student = new Student(32, "Way Lau", "17088888888", "Shenzhen"); // 添加学生 system.addStudent(student); // Add student success // 再次添加学生 system.addStudent(student); // Student exsit // 第一次查询所有学生 List<Student> studentList = system.getStudentList(); // 删除学生 system.removeStudent(student); // Remove student success // 再次删除学生 system.removeStudent(student); // Student not exsit // 查询所有学生 studentList = system.getStudentList(); // No student exsit } }运行上述程序,可以看到控制台输出内容如下:Add student success Student exsit Student info: name Way Lau, age 32, phone 17088888888, address Shenzhen Remove student success Student not exsit No student exsit程序语言描述算法一步到位,写出的算法可直接交予计算机处理。对于懂得这类程序语言的开发者而言,通过运行程序可以马上验证算法的正确性。当然其缺点也较为明显:不便于体现自顶向下、逐步求解的思想;程序语言包含很多细节内容,会淹没算法的主要思想。因此,在描述某个算法时,往往通过几种描述方式结合起来使用。参考引用原本同步至:https://waylau.com/description-of-algorithms/本系列归档:https://github.com/waylau/java-data-structures-and-algorithms-inaction数据结构和算法基础(Java语言实现):https://item.jd.com/13014179.html
《数据结构和算法基础(Java语言实现)》一书由北京大学出版社出版,已经于近日上市。拿到了样书,第一时间希望与读者朋友们分享下这本书里面的内容。为啥要写这本书12月6日拿到了样书,迫不及待的对新书做了浏览。同时也做了拆书与导读,可以在B站找到:https://www.bilibili.com/video/BV1fY411s7Kr/聊下为啥要写这本书。其实,这本是我所编写过的书目(https://waylau.com/books/)里面,算是最为“低级”的课题了吧,毕竟谁不知道“数据结构和算法”呢?这个课题太基础了。但是“数据结构和算法”却又是非常重要的课程。算法和数据结构是程序的灵魂,在计算机类培训课程中属于必开的课程。虽然实际工作中大多数人并不是专业的算法工程师,不以算法为深,但不可否认算法在工作中的重要性,初级工程师与高级工程师的差距也许就在对于算法的理解上。理解算法,运用合理的数据结构,可以让程序更加高效。随着云计算、大数据、人工智能、虚拟现实等应用的兴起,企业对于开发人员的算法技术要求也越来越高。不会算法或不精通算法,也许就会错过很多就业良机。另外,在求职时,算法是面试的必考类型。鉴于算法和数据结构在编程中的重要性,笔者迫不及待地希望将工作中常用的算法介绍给大家。因此,笔者陆续在个人开源网站https://github.com/waylau/java-data-structures-and-algorithms-inaction>上发表了众多关于算法的技术博客。2020年年底,笔者将之前算法相关的个人博客整理成册,遂有了本书。三大特点概况起来,这本《数据结构和算法基础(Java语言实现)》主要有三大特点。B站也有相关介绍:https://www.bilibili.com/video/BV1Lg411P7LP/1、涉及面非常广那么涉及广的话可以体现在哪里呢?可以看这本书的内容简介部分。该书分为以下几部分:第一部分 预备知识(第1-2章):介绍数据结构和算法的基本概念,并演示如何搭建开发环境、编写测试用例。第二部分 数据结构(第3-13章):介绍常见的数据结构,包括数组、链表、矩阵、栈、队列、跳表、散列、树、图等。第三部分 常用算法(第14-19章):介绍常用的算法,包括分而治之、动态规划、贪心算法、回溯、遗传算法、蚂蚁算法等。第四部分 商业实战(第20章):介绍汉诺塔游戏的实现。可以说,基本上你常见的一些是业务上还是技术常用的一些数据结构和算法,这本书都已经涉及了。更难能可贵的是,这本书也对当前非常火爆的诸如AI、机器学习等算法也做了讲解。2、图例非常丰富这本书是他这个图例非常丰富,从基本的IDE安装,到复杂的数据结构的演示,都有丰富的图例。那么在讲这种数据结构或者算法理论的时候,通过图例的配套讲解演示,可以方便读者理解。3、代码量非常大第三个特点的话就是它里面的实战案例非常丰富。实战案例体现在,这本书的每一章每个知识点基本上会配套一个实战案例,代码量是非常大的。每行代码这个重点代码它都有一些注释给你写得明明白白。这本书呢是不单只是简单的讲一些理论,它还有会手把手的教你写代码。理论联系实际。学习本书,一起手撕算法!参考引用原本同步至:https://waylau.com/java-data-structures-and-algorithms-in-action-book-three-features/京东有售:https://item.jd.com/13014179.html
JDK 17已经于2021年3月16日如期发布。本文介绍JDK 17新特性。JDK 17于2021年9月14日正式发布(General-Availability Release)。JDK 17将是大多数供应商的长期支持(LMS)版本。上一个LTS版本是JDK 11。本文总结了JDK 17发布的新特性。发布版本说明根据发布的规划,这次发布的 JDK 17 将是一个长期支持版(LTS 版)。LTS 版每 3 年发布一个,上一次长期支持版是 18 年 9 月发布的 JDK 11。 JDK 17是Java SE平台版本17的开源参考实现,由JSR 392在JCP(Java Community Process)指定。安装包下载主要分为OpenJDK版本和Oracle版本,下载地址如下:OpenJDK版本:https://jdk.java.net/16/Oracle版本:https://www.oracle.com/java/technologies/downloads/上述版本,如果是个人学习用途,则差异不大。但如果是用于商业用途,则需要仔细看好相关的授权。Oracle JDK根据二进制代码许可协议获得许可,而OpenJDK根据GPL v2许可获得许可。 更多有关Java的基本知识,可以参阅《Java核心编程》这本书,描述的非常详细。JDK 17 新特性说明JEP 406:switch的模式匹配(预览)(JDK-8213076)specification通过switch表达式和语句的模式匹配,以及模式语言的扩展,增强Java编程语言。将模式匹配扩展到switch允许对表达式进行测试,每个模式都有特定的操作,以便可以简洁而安全地表达复杂的面向数据的查询。有关更多详细信息,请参见JEP 406JEP 409:密封类(JDK-8260514)specification密封类(Sealed Class)已添加到Java语言中。密封类和接口限制了哪些其他类或接口可以扩展或实现它们。密封类由JEP 360并在JDK 15中作为预览功能交付。它们再次被提出,并进行了改进,由JEP 397并在JDK 16中作为预览功能提供。现在,在JDK 17中,密封类正在最终确定,与JDK 16没有任何更改。有关更多详细信息,请参见JEP 409JEP 382:新的macOS渲染管道(JDK-8238361)client-libs/2dSwing API用于渲染的Java 2D API现在可以使用新的Apple Metal加速渲染API 给macOS。目前默认情况下,这是禁用的,因此渲染仍然使用OpenGL API,这些API被Apple弃用,但仍然可用和支持。要启用金属,应用程序应通过设置系统属性指定其使用:-Dsun.java2d.metal=trueMetal或OpenGL的使用对应用程序是透明的,因为这是内部实现的区别,对Java API没有影响。Metal管道需要macOS 10.14.x或更高版本。在早期版本上设置它的尝试将被忽略。有关更多详细信息,请参见[JEP 382(https://openjdk.java.net/jeps/382)大图标访问新API(JDK-8182043)client-libs/javax.swingJDK 17中提供了一个新的方法javax.swing.filechooser.FileSystemView.getSystemIcon(File, int, int),该方法允许在可能的情况下访问更高质量的图标。它已为Windows平台完全实施;但是,其他平台上的结果可能会有所不同,稍后将增强。例如,通过使用以下代码:FileSystemView fsv = FileSystemView.getFileSystemView(); Icon icon = fsv.getSystemIcon(new File("application.exe"), 64, 64); JLabel label = new JLabel(icon);用户可以为“application.exe”文件获得更高质量的图标。此图标适用于创建可以在HighDPI环境中更好地扩展的标签。DatagramSocket可以直接加入多播组(JDK-8237352)core-libs/java.netjava.net.DatagramSocket在此版本中已更新,以添加对加入多播组(multicast group)的支持。它现在定义了加入和离开多播组的加入组和离开组方法。java.net.DatagramSocket的类级API文档已更新,以解释如何配置普通DatagramSocket并用于加入和离开多播组。此更改意味着DatagramSocket API可以用于组播应用程序,而无需使用旧的java.net.MulticastSocket API。MulticastSocket API的工作原理和以前一样,尽管它的大多数方法都被弃用了。有关此变更理由的更多信息,请查看CSRJDK-8260667JEP 356:增强型伪随机数生成器(JDK-8193209)core-libs/java.util为伪随机数生成器(PRNG)提供新的接口类型和实现,包括可跳转的PRNG和一类额外的可拆分PRNG算法(LXM)。有关更多详细信息,请参见JEP 356Ideal Graph Visualizer的现代化(JDK-8254145)hotspot/compilerIdeal Graph Visualizer(IGV)是一个可视化和交互式地探索HotSpot VM C2即时(JIT)编译器中使用的中间表示的工具,已经现代化。增强功能包括:支持在最多JDK 15上运行IGV(IGV底层NetBeans平台支持的最新版本)更快的、基于Maven的IGV构建系统块形成、组删除和节点跟踪的稳定默认过滤器中更直观的着色和节点分类具有更自然默认行为的排名快速节点搜索现代化的IGV部分兼容从早期JDK版本生成的图形。它支持基本功能,如图形加载和可视化,但辅助功能,如节点聚类和着色可能会受到影响。有关构建和运行IGV的详细信息,请参见https://github.com/openjdk/jdk17/tree/master/src/utils/IdealGraphVisualizer。“New API”的新页面和改进的“Deprecated”页(JDK-8263468)tools/javadoc(tool)JavaDoc现在可以生成一个页面,总结API中最近的更改。要包括的最近版本的列表是使用 --since命令行选项指定的。这些值用于查找具有匹配@since的声明,因为要包含在新页面上的标记。--since-label命令行选项提供了要在“New API”页面标题中使用的文本。在总结已弃用项目的页面上,您可以查看按已弃用项目的版本分组的项目。错误消息中的源详细信息(JDK-8267126)tools/javadoc(tool)当JavaDoc报告输入源文件中的问题时,它将以类似编译器(javac)诊断消息的方式显示问题的源行,以及包含指向该行位置的插入符号(^)的行。此外,日志记录和其他“信息”消息现在写入标准错误流,留下标准输出流用于命令行选项特别请求的输出,如命令行帮助。JEP 412:外部函数和内存API(孵化)(JDK-8265033)core-libs引入一个API,Java程序可以通过该API与Java运行时之外的代码和数据互操作。通过有效地调用外部函数(即JVM外部的代码),并通过安全地访问外部内存(即不由JVM管理的内存),该API使Java程序能够调用本机库并处理本机数据,而不会有JNI的脆弱性和危险。有关更多详细信息,请参阅JEP 412控制台字符集API(JDK-8264208)core-libsjava.io.Console已更新,以定义一个新方法,该方法返回控制台的Charset。返回的Charset可能与Charset.defaultCharset()方法返回的Charset不同。例如,它返回IBM437,而Charset.defaultCharset()在Windows (en-US)上返回windows-1252。请参阅https://bugs.openjdk.java.net/browse/JDK-8264209了解更多详细信息。用于反序列化的JDK Flight Recorder事件(JDK-8261160)core-libs/java.io:serialization现在可以使用JDK Flight Recorder (JFR)监控对象的反序列化。当启用JFR且JFR配置包括反序列化事件时,每当运行程序尝试反序列化对象时,JFR将发出事件。反序列化事件名为jfr.Derialization,默认情况下禁用。反序列化事件包含序列化筛选器机制使用的信息;请参阅对象输入筛选器规范。此外,如果启用了过滤器,JFR事件指示过滤器是接受还是拒绝对象的反序列化。有关如何使用JFR反序列化事件的更多信息,请参阅文章监控反序列化提高应用安全性。有关使用和配置JFR的参考信息,请参阅JFR运行时指南和JFR命令参考JDK任务控制文件的章节。JEP 415:实现特定于上下文的反序列化过滤器(JDK-8264859)core-libs/java.io:serializationJEP 415:特定于上下文的反序列化过滤器允许应用程序通过JVM范围的过滤器工厂配置特定于上下文的和动态选择的反序列化过滤器,该工厂被调用以为每个单独的反序列化操作选择过滤器。用于序列化过滤的Java核心库开发人员指南介绍了用例,并提供了示例。本机字符编码名称的系统属性(JDK-8265989)core-libs/java.lang引入了一个新的系统属性本机.encode。此系统属性提供基础主机环境的字符编码名称。例如,它通常在Linux和macOS平台中具有UTF-8,在Windows (en-US)中具有Cp1252。请参阅https://bugs.openjdk.java.net/browse/JDK-8266075了解更多详细信息。添加java.time.InstantSource (JDK-8266846)core-libs/java.time引入了一个新的接口java.time.InstantSource。此接口是java.time.Clock的抽象,只关注当前时刻,不引用时区。十六进制格式和解析实用程序(JDK-8251989)core-libs/java.utiljava.util.HexFormat为基元类型和字节数组提供十六进制和十六进制之间的转换。分隔符、前缀、后缀和大写或小写的选项由返回HexFormat实例的工厂方法提供。实验Compiler Blackholes支持(JDK-8259316)hotspot/compiler增加了对Compiler Blackholes的实验支持。这些对于低级基准测试非常有用,以避免关键路径上的死代码消除,而不影响基准性能。当前的支持以CompileCommand的形式实现,可访问为-XX:CompileCommand=blackhole,,并计划最终将其毕业到公共API。JMH已经能够在指示/可用时自动检测和使用此设施。有关后续步骤,请查阅JMH文档。HotSpot JVM中的新类层次结构分析实现(JDK-8266074)hotspot/compilerHotSpot JVM中引入了一个新的类层次结构分析实现。它的特点是对抽象和默认方法的增强处理,从而改进了JIT编译器所做的内联决策。新实现将取代原始实现,并在默认情况下打开。为了帮助诊断与新实现相关的可能问题,可以通过指定 -XX:+UnlockDiagnosticVMOptions -XX:-UseVtableBasedCHA命令行标志来打开原始实现。原始实现可能会在未来的版本中删除。JEP 391: macOS/AArch64端口(JDK-8251280)hotspot/compilermacOS 11.0现在支持AArch64体系结构。此JEP在JDK中实现了对macos-aarch64平台的支持。添加的功能之一是支持W^X(write xor execute)内存。它仅对macos-aarch64启用,并可以在某些时候扩展到其他平台。JDK可以在英特尔计算机上交叉编译,也可以在基于Apple M1的计算机上编译。有关更多详细信息,请参见JEP 391统一日志支持异步日志刷新(JDK-8229517)hotspot/runtime为了避免使用统一日志记录的线程中出现不希望的延迟,用户现在可以请求统一日志记录系统在异步模式下运行。这可以通过传递命令行选项-Xlog:async来完成。在异步日志记录模式下,日志站点将所有日志记录消息入队到缓冲区。独立线程负责将它们刷新到相应的输出。中间缓冲区是有界的。缓冲区耗尽时,入队消息将被丢弃。用户可以使用命令行选项-XX:AsyncLogBufferSize=.来控制中间缓冲区的大小。ARM上的macOS早期访问可用(JDK-8266858)infrastructure/build新的macOS现在可用于ARM系统。ARM端口的行为应与英特尔端口类似。没有已知的功能差异。在macOS上报告问题时,请指定是使用ARM还是x64。支持在Keytool -genkeypair命令中指定签名者(JDK-8260693)security-libs/java.security-signer和-signerkeypass选项已添加到keytool实用程序的-genkey对命令中。-signer选项指定签名者的私钥条目的密钥库别名,-signerkeypass选项指定用于保护签名者私钥的密码。这些选项允许keytool -genkey对使用签名者的私钥对证书进行签名。这对于生成具有密钥协商算法作为公钥算法的证书特别有用。SunJCE提供程序通过AES密码支持KW和KWP模式(JDK-8248268)security-libs/javax.cryptoSunJCE提供程序已得到增强,以支持AES密钥换行算法(RFC 3394)和带填充算法的AES密钥换行算法(RFC 5649)。在早期版本中,SunJCE提供程序在“AESWrap”密码算法下支持RFC 3394,该算法只能用于包装和解包装密钥。通过此增强,增加了两种分组密码模式,KW和KWP,支持使用AES进行数据加密/解密和密钥包装/解包装。有关更多详细信息,请查看“JDK提供程序文档”指南的“SunJCE提供程序”部分。新SunPKCS11配置属性(JDK-8240256)security-libs/javax.crypto:pkcs11SunPKCS11提供程序添加了新的提供程序配置属性,以更好地控制本机资源的使用。SunPKCS11提供程序使用本机资源以便与本机PKCS11库一起工作。为了管理和更好地控制本机资源,添加了额外的配置属性,以控制清除本机引用的频率,以及是否在注销后销毁基础PKCS11令牌。SunPKCS11提供程序配置文件的3个新属性是:destroyTokenAfterLogout (布尔值,默认值为false)如果设置为true,则在SunPKCS11提供程序实例上调用java.security.AuthProvider.logout() 时,基础令牌对象将被销毁,资源将被释放。这基本上会在logout() 调用后使SunPKCS11提供程序实例不可用。请注意,不应将此属性设置为true的PKCS11提供程序添加到系统提供程序列表中,因为提供程序对象在logout() 方法调用后不可用。cleaner.shortInterval(整数,默认值为2000,以毫秒为单位)这定义了在繁忙期间清除本机引用的频率,即cleaner线程应多久处理队列中不再需要的本机引用以释放本机内存。请注意,cleaner线程将在200次失败尝试后切换到“longInterval”频率,即在队列中找不到引用时。cleaner.longInterval(整数,默认值为60000,以毫秒为单位)这定义了在非繁忙期间检查本机引用的频率,即cleaner线程应检查队列中的本机引用的频率。请注意,如果检测到用于清理的本机PKCS11引用,cleaner线程将切换回“短间隔”值。具有系统属性的可配置扩展(JDK-8217633)security-libs/javax.net.ssl已添加两个新的系统属性。系统属性jdk.tls.client.disableExts用于禁用客户端中使用的TLS扩展。系统属性jdk.tls.server.disableExts用于禁用服务器中使用的TLS扩展。如果禁用了扩展,则在握手消息中既不会生成也不会处理扩展。属性字符串是在IANA文档中注册的逗号分隔的标准TLS扩展名称列表(例如,server_name、status_request和签名_algorithms_cert)。请注意,扩展名区分大小写。未知、不支持、拼写错误和重复的TLS扩展名称令牌将被忽略。请注意,阻止TLS扩展的影响是复杂的。例如,如果禁用了强制扩展,则可能无法建立TLS连接。请不要禁用强制扩展,除非您清楚地了解其影响,否则不要使用此功能。包摘要页面上的“Related Packages”(JDK-8260388)tools/javadoc(tool)软件包的摘要页面现在包括一个列出任何“Related Packages”的部分。Related Packages(相关软件包)是根据常见命名约定启发式确定的,可能包括以下内容:“parent”包(即,包是子包的包)同级包(即具有相同父包的其他包)任何子包相关软件包不一定都在同一个模块中。参考引用本文同步至: https://waylau.com/jdk-17-released/https://waylau.com/jdk-16-released/https://waylau.com/jdk-15-released/https://waylau.com/jdk-14-released/《Java核心编程》开源项目“现代Java案例大全” https://github.com/waylau/modern-java-demos
随着业务数据的增加,原有的数据库性能瓶颈凸显,以此就需要对数据库进行分库分表操作。为啥需要分库分表随着业务数据的增加,原有的数据库性能瓶颈凸显,主要体现在以下两个方面。IO瓶颈IO瓶颈主要有以下几种情况:第一种:磁盘读IO瓶颈,热点数据太多,数据库缓存放不下,每次查询时会产生大量的IO,降低查询速度。这种情况适合采用分库和垂直分表。第二种:网络IO瓶颈,请求的数据太多,网络带宽不够。这种情况适合采用分库。CPU瓶颈CPU瓶颈主要有以下几种情况:第一种:SQL问题,如SQL中包含join,group by,order by,非索引字段条件查询等,增加CPU运算的操作。这种情况适合采用SQL优化,建立合适的索引,或者把一些SQL操作移到在业务层中台代码中去做业务计算。第二种:单表数据量太大,查询时扫描的行太多,SQL效率低,CPU率先出现瓶颈这种情况适合采用水平分表。综上,大多数情况下,需要使用数据库的分库分表方案来解决性能瓶颈。理解分库分表“分库分表”本质就是把数据分到不同的数据库或者分到不同的数据表上,以减轻单库或者单表的数据量,从而降低访问单库或者单表时的数据压力。在理解了分库分表的重要性之后,那么来理解下分库分表的实现原理。水平分库水平分库是指,以字段为依据,按照一定策略(hash、range等),将一个库中的数据拆分到多个库中。比如以下的例子。对用户表进行水平分库,分库的策略是对user_id字段进行取模。如果取模结果是0,则放入数据库01;如果取模结果是1,则放入数据库02。水平分库的结果是:每个库的结构都一样;每个库的数据都不一样,没有交集;所有库的并集是全量数据。水平分库适用的场景是,系统绝对并发量上来了,分表难以根本上解决问题,并且还没有明显的业务归属来垂直分库。水平分表水平分表是指,以字段为依据,按照一定策略(hash、range等),将一个表中的数据拆分到多个表中。比如以下的例子。对用户表user_t进行水平分表,分库的策略是对user_id字段进行取模。如果取模结果是0,则放入user_t_01表;如果取模结果是1,则放入user_t_02表。水平分表的结果是:每个表的结构都一样;每个表的数据都不一样,没有交集;所有表的并集是全量数据。水平分表适用的场景是,系统绝对并发量并没有上来,只是单表的数据量太多,影响了SQL效率,加重了CPU负担,以至于成为瓶颈。垂直分库垂直分库是指,以表为依据,按照业务归属不同,将不同的表拆分到不同的库中。比如以下的例子。用户业务相关的表放入到01库,订单业务相关的表放入到02库。垂直分库的结果是:每个库的结构都不一样;每个库的数据也不一样,没有交集;所有库的并集是全量数据。垂直分库适用的场景是,系统绝对并发量上来了,并且可以抽象出单独的业务模块。到这一步,基本上就可以服务化了。例如,随着业务的发展一些公用的配置表、字典表等越来越多,这时可以将这些表拆到单独的库中,甚至可以服务化。再有,随着业务的发展孵化出了一套业务模式,这时可以将相关的表拆到单独的库中,甚至可以服务化或者微服务化。垂直分表垂直分表是指,以字段为依据,按照字段的活跃性,将表中字段拆到不同的表(主表和扩展表)中。垂直分表的结果是:每个表的结构都不一样;每个表的数据也不一样,一般来说,每个表的字段至少有一列交集,一般是主键,用于关联数据;所有表的并集是全量数据。垂直分表适用的场景是,系统绝对并发量并没有上来,表的记录并不多,但是字段多,并且热点数据和非热点数据在一起,单行数据所需的存储空间较大。以至于数据库缓存的数据行减少,查询时会去读磁盘数据产生大量的随机读IO,产生IO瓶颈。比如以下“新闻头条”应用的例子,“新闻头条”分为了新闻列表页和新闻详情页。垂直分表的拆分原则是将热点数据(比如新闻的标题)放在一起作为主表(news_t),非热点数据(新闻的内容)放在一起作为扩展表(news_ext_t)。这样更多的热点数据就能被缓存下来,进而减少了随机读IO。拆了之后,要想获得全部数据就需要关联两个表来取数据。需要注意的是,垂直分表关联两个表查询的时候,避免使用join,因为join不仅会增加CPU负担并且会讲两个表耦合在一起(必须在一个数据库实例上)。关联数据,尽量是放在业务层中台来做。分库分表的几种分配策略hash取模比如,对用户表user_t进行水平分表,分库的策略是对user_id字段进行取模。如果取模结果是0,则放入user_t_01表;如果取模结果是1,则放入user_t_02表。范围分片(range)比如,user_id从1到10000作为一个分片,从10001到20000作为另一个分片。地理位置分片华南区一个分片,华北一个分片。时间分片按月、季度、年分片等等,可以做到冷热数据。比如,今年内的数据一般就是热数据,而往年的数据就是冷数据。那么可以分为 user_t_2021、user_t_2020等表,user_t_2021是热数据,user_t_2020为冷数据。参考引用本文同步至: https://waylau.com/database-sharding/https://shardingsphere.apache.org/document/current/en/overview/
本文介绍了MyBatis的${}和#{}的用法区别,以及针对$可能带来的风险提供一种简易的SQL防注入的方法。#{}用法select语句是MyBatis中最常用的元素之一,例如:<select id="selectPerson" parameterType="int" resultType="hashmap"> SELECT * FROM PERSON WHERE ID = #{id} </select>此语句称为selectPerson,采用int(或Integer)类型的参数,并将查询结果封装为HashMap作为返回。语句中#{id}这告诉MyBatis创建一个PreparedStatement参数。对于JDBC而言,这样的参数类似于PreparedStatement语句中的“?”标识,如下:// JDBC代码 String selectPerson = "SELECT * FROM PERSON WHERE ID=?"; PreparedStatement ps = conn.prepareStatement(selectPerson); ps.setInt(1,id);${}用法默认情况下,使用#{}语法将导致MyBatis生成PreparedStatement属性,并根据PreparedStatement参数安全地设置值(例如“?”标识)。虽然这更安全、更快,而且几乎总是首选,但有时只是想直接将未修改的字符串注入SQL语句。例如,对于订单排序,可以使用类似的内容:ORDER BY ${columnName}在上面的用法中,MyBatis不会修改或转义字符串。但需要注意的是: ${}用法如果是直接接受用户的输入,将未经修改的语句注入到程序是不安全的。这将导致潜在的SQL注入攻击风险。针对${}的SQL防注入器${}用法存在SQL注入的风险,因此需要对用户的输入内容进行校验。这里提供一个简易的SQL防注入的方法。import java.util.regex.Matcher; import java.util.regex.Pattern; /** * SQL注入防护器 * * @author waylau.com * @since 2021-04-18 */ public class SqlInjectionProtector { private static final String SPECIAL_CHAR = "\\W"; /** * 校验SQL语句是否合法 * 如果非法,则抛出异常 * * @param statement 语句 * @return 是否合法 * @throws IllegalArgumentException 校验非法则抛出此异常 */ public static boolean verifySqLStatement(String statement) { if (!StringUtility.isEmpty(statement)) { if (isSpecialChar(statement)) { throw new IllegalArgumentException("illegal statement: " + statement); } } return Boolean.TRUE; } /** * 判断是否含有特殊字符 * * @param str 校验的字符串 * @return 是否特殊字符 */ public static boolean isSpecialChar(String str) { Pattern pattern = Pattern.compile(SPECIAL_CHAR); Matcher matcher = pattern.matcher(str); return matcher.find(); } }上述代码核心思想是,通过正则表达式的方式,来检测出特殊字符。有特殊字符,就抛出异常,中断程序继续往下运行。何为特殊字符?针对ORDER BY ${columnName} 这个例子而言,字段名的所使用的字符是有一定限制的,限制只能使用[a-z0-9A-Z_]这个范围内的字符。因此超出这个范围内的所有字符,即为特殊字符。在正则表达式里面,非[a-z0-9A-Z_]范围内的字符,可以用“\W”表示。以下是测试用例import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import org.junit.Test; /** * SqlInjectionProtector Test * * @author waylau.com * @since 2021-04-18 */ public class SqlInjectionProtectorTest { @Test public void testIsSpecialChar() { assertFalse(SqlInjectionProtector.isSpecialChar("user_name")); assertTrue(SqlInjectionProtector.isSpecialChar("user_name!")); assertTrue(SqlInjectionProtector.isSpecialChar("user_name@")); assertTrue(SqlInjectionProtector.isSpecialChar("user_name^")); assertTrue(SqlInjectionProtector.isSpecialChar("user_name ")); // 空格 assertTrue(SqlInjectionProtector.isSpecialChar("insert\ninto\nuser_t")); // 换行符 assertTrue(SqlInjectionProtector.isSpecialChar("user_name|user_t")); } @Test public void testVerifySqLStatement() { assertTrue(SqlInjectionProtector.verifySqLStatement("user_name")); } }参考引用本文同步至: https://waylau.com/mybatis-parameters-and-sql-inject-protector/《轻量级Java EE企业应用开发实战》(https://item.jd.com/12817685.html)《大型互联网应用轻量级架构实战》(https://item.jd.com/12629095.html)
JDK 16已经于2021年3月16日如期发布。本文介绍JDK 16新特性。发布版本说明根据发布的规划,这次发布的 JDK 16 将是一个短期的过度版。下一个长期支持版(LTS 版)会在今年的 9 月份候发布(Java 17),LTS 版每 3 年发布一个,上一次长期支持版是 18 年 9 月发布的 JDK 11。 JDK 16是Java SE平台版本16的开源参考实现,由JSR 390在JCP(Java Community Process)指定。安装包下载主要分为OpenJDK版本和Oracle版本,下载地址如下:OpenJDK版本:https://jdk.java.net/16/Oracle版本:https://www.oracle.com/java/technologies/javase-jdk16-downloads.html上述版本,如果是个人学习用途,则差异不大。但如果是用于商业用途,则需要仔细看好相关的授权。Oracle JDK根据二进制代码许可协议获得许可,而OpenJDK根据GPL v2许可获得许可。 更多有关Java的基本知识,可以参阅《Java核心编程》这本书,描述的非常详细。JDK 16 新特性说明JDK 16 为用户提供了17项主要的增强/更改,包括全新的 Java 语言改进,工具和内存管理,以及还有一些孵化和预览特性,有了这些新功能,Java 会进一步提高开发人员的生产力。值得关注的变化是,JDK 14 中提供的预览特性:模式匹配和记录(Records),经过一年的社区反馈和实际应用,终于在 JDK 16 中完成最终落地了。另外,Oracle 还为 Java SE 订阅服务中免费提供 GraalVM 企业版服务,GraalVM 可以帮助提高应用程序的性能并减少资源消耗,尤其是在微服务和云原生架构中。1. 338: Vector API (孵化)这个不是集合中的Vector,而是一个新的初始迭代孵化器模块jdk.incubator.vector,用于表示在运行时可靠地编译到支持的 CPU 架构上的最佳矢量硬件指令的矢量计算。2. 347: Enable C++14 Language Features允许在 JDK 底层的C++源代码中使用C++14的新语言特性,并且提供了在HotSpot虚拟机代码中,哪些代码使用了这些新特性的指南。3. 357: Migrate from Mercurial to Git将 OpenJDK 社区的源代码存储库从 Mercurial 迁移到 Git。4. 369: Migrate to GitHub在 GitHub 上托管 OpenJDK 社区的 Git 存储库。GitHub 是世界流行的Git代码托管平台。在国内,托管代码推荐Gitee哦。5. 376: ZGC: Concurrent Thread-Stack ProcessingZGC 最早是在 JDK 11 中集成进来的,在 JDK 15 中正式转正。这个版本则是为了让 ZGC 支持并发栈处理,解决了最后一个重大瓶颈,把 ZGC 中的线程栈处理从安全点移到了并发阶段。并且还提供了一种机制,使得其他 HotSpot 子系统可以通过该机制延迟处理线程栈。6. 380: Unix-Domain Socket ChannelsUNIX 域套接字通道,为 java.nio.channels 包中的套接字通道和服务端套接字通道 APIs 增加 Unix 域套接字通道所有特性支持。UNIX 域套接字主要用于同一主机上的进程间通信(IPC),大部分方面与 TCP/IP套接字类似,不同的是 UNIX 域套接字是通过文件系统路径名寻址,而不是通过 IP 地址和端口号。7. 386: Alpine Linux Port在 x64 和 AArch64 平台体系结构上,将 JDK 移植到 Alpine Linux 以及使用 musl 作为其主要 C 语言库的其他 Linux 发行版中。8. 387: Elastic Metaspace弹性的元空间,可以帮助 HotSpot 虚拟机,将元空间中未使用的 class 元数据内存更及时地返回给操作系统,以减少元空间的内存占用空间。另外,还简化了元空间的代码,以降低维护成本。9. 388: Windows/AArch64 Port将 JDK 移植到 Windows/ AArch64 平台系列。10. 389: Foreign Linker API (孵化)引入了一个新的 API,该 API 提供了对本地 native 代码的静态类型访问支持。11. 390: Warnings for Value-Based Classes基于值的类的警告,将基础类型包装类指定为基于值的类,废除其构造函数以进行删除,从而提示新的弃用警告。并且提供了在任何基于值的类的实例上不正常进行同步的警告。12. 392: Packaging Tool提供了 jpackage 打包工具,可用于打包独立的 Java 应用程序。jpackage 打包工具是在 JDK 14 中首次作为孵化工具引入的新特性,到了 JDK 15 它仍然还在孵化中,现在它终于转正了。13. 393: Foreign-Memory Access API (三次孵化)该 API 允许 Java 应用程序安全有效地访问 Java 堆之外的外部内存。这个最早在 JDK 14 中成为孵化特性,JDK 15/ JDK 16 中继续二、三次孵化并对其 API 有了一些更新,这个可以在 JDK 17 中好好期待一下转正。14. 394: Pattern Matching for instanceof模式匹配 for instanceof,相当于是增强的 instanceof,在 JDK 14 中首次成为预览特性,在 JDK 16 中正式转正。模式匹配的到来将使得 instanceof 变得更简洁、更安全,为什么这么说,请看下面的示例。Java 14 之前用法:if (obj instanceof String) { String s = (String) obj; // 使用s }Java 14之后的用法:if (obj instanceof String s) { // 使用s }15. 395: Records简单来说,Records 就是一种新的语法糖,目的还是为了简化代码,在 JDK 14 中首次成为预览特性,在 JDK 16 中正式转正。Records 可以在一定程度上避免低级冗余的代码,比如:constructors, getters, equals(), hashCode(), toString() 方法等,相当于 Lombok 的 @Data 注解,但又不能完全替代。下面来看一个示例:旧写法:class Point { private final int x; private final int y; Point(int x, int y) { this.x = x; this.y = y; } int x() { return x; } int y() { return y; } public boolean equals(Object o) { if (!(o instanceof Point)) return false; Point other = (Point) o; return other.x == x && other.y = y; } public int hashCode() { return Objects.hash(x, y); } public String toString() { return String.format("Point[x=%d, y=%d]", x, y); } }新的Records写法:record Point(int x, int y) { }16. 396: Strongly Encapsulate JDK Internals by DefaultJDK 内部默认强封装,JDK 16 开始对 JDK 内部大部分元素默认进行强封装,sun.misc.Unsafe 之类的关键内部 API 除外,从而限制对它们的访问。此外,用户仍然可以选择自 JDK 9 以来的默认的宽松的强封装,这样可以帮助用户毫不费力地升级到未来的 Java 版本。17. 397: Sealed Classes (二次预览)封闭类(二次预览),可以是封闭类和或者封闭接口,用来增强 Java 编程语言,防止其他类或接口扩展或实现它们。参考引用本文同步至: https://waylau.com/jdk-16-released/https://waylau.com/jdk-15-released/https://waylau.com/jdk-14-released/《Java核心编程》https://github.com/waylau/modern-java-demos
ChannelHandler(管道处理器)其工作模式类似于Java Servlet过滤器,负责对I/O事件或者I/O操作进行拦截处理。采用事件的好处是,ChannelHandler可以选择自己感兴趣的事件进行处理,也可以对不感兴趣的事件进行透传或者终止。ChannelHandler接口基于ChannelHandler接口,用户可以方便实现自己的业务,比如记录日志、编解码、数据过滤等。ChannelHandler接口定义如下:package io.netty.channel; import io.netty.util.Attribute; import io.netty.util.AttributeKey; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Inherited; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; public interface ChannelHandler { void handlerAdded(ChannelHandlerContext ctx) throws Exception; void handlerRemoved(ChannelHandlerContext ctx) throws Exception; void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception; @Inherited @Documented @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @interface Sharable { } }ChannelHandler接口定义比如简单,只有三个方法:handlerAdded方法在ChannelHandler被添加到实际上下文中并准备好处理事件后调用。handlerRemoved方法在ChannelHandler从实际上下文中移除后调用,表明它不再处理事件。exceptionCaught方法会在抛出Throwable类后调用。还有一个Sharable注解,该注解用于表示多个ChannelPipeline可以共享同一个ChannelHandler。正式因为ChannelHandler接口过于简单,我们在实际开发中,不会直接实现ChannelHandler接口,因此,Netty提供了ChannelHandlerAdapter抽象类。ChannelHandlerAdapter抽象类ChannelHandlerAdapter抽象类核心代码如下:package io.netty.channel; import io.netty.util.internal.InternalThreadLocalMap; import java.util.Map; import java.util.WeakHashMap; public abstract class ChannelHandlerAdapter implements ChannelHandler { boolean added; public boolean isSharable() { Class<?> clazz = getClass(); Map<Class<?>, Boolean> cache = InternalThreadLocalMap.get().handlerSharableCache(); Boolean sharable = cache.get(clazz); if (sharable == null) { sharable = clazz.isAnnotationPresent(Sharable.class); cache.put(clazz, sharable); } return sharable; } @Override public void handlerAdded(ChannelHandlerContext ctx) throws Exception { // NOOP } @Override public void handlerRemoved(ChannelHandlerContext ctx) throws Exception { // NOOP } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { ctx.fireExceptionCaught(cause); } }ChannelHandlerAdapter对exceptionCaught方法做了实现,并提供了isSharable方法。需要注意的是,ChannelHandlerAdapter是抽象类,用户可以自由的选择是否要覆盖ChannelHandlerAdapter类的实现。如果对某个方法感兴趣,直接覆盖掉这个方法即可,这样代码就变得简单清晰。ChannelHandlerAdapter抽象类提供了两个子类ChannelInboundHandlerAdapter、ChannelOutboundHandlerAdapter用于针对出站事件、入站事件的进行处理。其中ChannelInboundHandlerAdapter实现了ChannelInboundHandler接口,而ChannelOutboundHandlerAdapter实现了ChannelOutboundHandler接口。在实际开发过程中,我们的自定义的ChannelHandler多数是继承自ChannelInboundHandlerAdapter和ChannelOutboundHandlerAdapter类或者是这两个类的子类。比如在前面章节中所涉及的编解码器ByteToMessageDecoder、MessageToMessageDecoder、MessageToByteEncoder、MessageToMessageEncoder等,就是这两个类的子类。参考引用原文同步至https://waylau.com/netty-channel-handler《Netty原理解析与开发实战》
IntelliJ IDEA一个吸引人的地方在于,他有比较好的反编译工具,这让Eclipse用户牙痒痒。但不要紧,本文介绍如何在Eclipse IDE中使用IntelliJ IDEA的反编译工具Fernflower。 为啥需要反编译 很多jar不提供源码,那么打开class是这个鸟样。 不具备人类可读性。因此需要反编译。 什么是Fernflower 那么我是怎么知道Fernflower的呢?你随便用IntelliJ IDEA打开一个jar中的class文件,可以看到下面的信息,这就是IEDA中的反编译工具Fernflower。 看官网介绍Fernflower(https://github.com/JetBrains/intellij-community/tree/master/plugins/java-decompiler/engine) Fernflower is the first actually working analytical decompiler for Java and probably for a high-level programming language in general “Fernflower是第一个真正为Java工作分析反编译器,通常也适用于一般的高级编程语言” 看介绍是很牛批的样子,当然实际也是。 如何获取Fernflower 非常遗憾的是,Fernflower是IntelliJ IDEA独家所有,那我是怎么搞定的呢? 我先在eclipse市场找了下,没有找到Fernflower,却找到了Enhanced Class Decompiler 看官网介绍(https://marketplace.eclipse.org/content/enhanced-class-decompiler) Enhanced Class Decompiler integrates JD, Jad, FernFlower, CFR, Procyon seamlessly with Eclipse and allows Java developers to debug class files without source code directly. It also integrates with the eclipse class editor, m2e plugin, supports Javadoc, reference search, library source attaching, byte code view and the syntax of JDK8 lambda expression. 简言之,Enhanced Class Decompiler集JD、Jad、FernFlower、CFR、Procyon等各种反编译工具之大成。换言之,FernFlower就是我Enhanced Class Decompiler的一个子集呗。 呵呵,好一招曲线救国。用Enhanced Class Decompiler变相用了FernFlower。 如何在Eclipse IDE中安装Fernflower 1. 在线安装 这是最简单的方式。使用Eclipse的同学都懂。 安装地址是: https://ecd-plugin.github.io/update 2. 离线安装 获取离线安装包zip文件(见附件),在“Add Repository”中指定该zip文件即可。 3. 可选组件 一般就选Core就够用了,不嫌多就全选上。 装完重启Eclipse就能看到这个工具了。 怎么使用Fernflower 右键class文件,使用如何在Eclipse IDE中安装FernFlower打开即可 反编译成功!看到庐山真面目了。 参考引用 原文同步至 https://waylau.com/eclipse-install-fernflower/
正常情况下,在Java中入参是不建议用做返回值的。除了造成代码不易理解、语义不清等问题外,可能还埋下了陷阱等你入坑。 问题背景 比如有这么一段代码: @Named public class AService { private SupplyAssignment localSupply = new SupplyAssignment(); @Inject private BService bervice; public List<Supply> calcSupplyAssignment() List<Supply> supplyList = bService.getLocalSupplyList(this.localSupply); … return supplyList; } } 上面代码,服务A希望调用服务B,以获取supplyList,但同时,服务A又希望修改localSupply的状态值,未能避免修改calcSupplyAssignment接口的(不想改返回的类型),将localSupply作为了入参但同时也用作了返回值。 服务B代码如下: @Named public class BService { public List<Supply> getLocalSupplyList (SupplyAssignment localSupply) SupplyAssignment supplyAssignment = this.getSupplyAssignment(); // 希望localSupply被重新赋值后返回 localSupply = supplyAssignment; … return supplyList; } } 在服务B代码内部,服务A的入参localSupply被传入,希望重新被supplyAssignment赋值而后返回新值。然而,这样做是无效的。 问题原因 先来看下编程语言中关于参数传递的类型: 值传递(pass by value)是指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。 引用传递(pass by reference)是指在调用函数时将实际参数的地址直接传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数。 因为Java程序设计语言是采用的值传递,因为Java没有指针的概念。也就是说方法得到的是所有参数值的一个拷贝,方法并不能修改传递给它的任何参数变量的内容。 因此,上述代码中,服务A调用服务B时,服务B的参数localSupply实际上是服务A的localSupply的一个拷贝,当然,这两个都是指向了同一个地址对象supplyAssignment1。 当在服务B内部对参数localSupply进行重新赋值是localSupply = supplyAssignment,实际上,只是对B的参数localSupply做了从新赋值,B的参数localSupply会指向一个新的地址对象supplyAssignment2。 从上图可以清晰看到,因此,服务A的localSupply和B的参数localSupply已经指向了不同的对象了,对B的参数localSupply做任何的修改,都不会影响服务A的localSupply的原值。这就是问题的原因,你希望服务B来修改服务A入参的状态,并将改后的值返回给服务A,但并不奏效。 解决方案 方案1:入参不要用作返回值 当然,这个是最清晰的且易于理解的,但这会导致有的接口的返回类型产生变化。 有时确实想要入参做返回值,那看方案2。 方案2:入参不要赋值新对象 这个方案就是直接在入参的对象上做状态的修改,而不要去赋值新对象。还是这个图: 在这个图中,只要我们是一直在B的参数localSupply修改的是supplyAssignment1的状态值,那结果就能反馈到服务A的localSupply上。如何实现?看下下面代码: @Named public class BService { public List<Supply> getLocalSupplyList (SupplyAssignment localSupply) SupplyAssignment supplyAssignment = this.getSupplyAssignment(); // 针对localSupply不能新建引用,只能重新赋值属性 BeanUtils.copyProperties(supplyAssignment, localSupply); … return supplyList; } } 在上面的方法中,我们用到了Spring的工具类BeanUtils,该类的copyProperties方法的实质是将supplyAssignment的属性值,赋值到了localSupply的属性上。这意味着我们是修改的B的参数localSupply上的属性,而并未新建对象。 参考引用 原文同步至 https://waylau.com/trap-in-java-use-param-for-return/ Java核心编程
今天遇到一个奇怪的Java三元表达式中的空指针异常。特此记录。 代码 代码示意如下: Integer itemVO = null; Integer globleLatenessToleranceUseAlternate = null; Integer latenessToleranceUseAlternate = (itemVO == null ? globleLatenessToleranceUseAlternate : itemVO.intValue()); 从上面代码可以看出:当itemVO不为空时,就取itemVO的值;否则,就取globleLatenessToleranceUseAlternate的值。 原因 但问题就在globleLatenessToleranceUseAlternate。当itemVO为空时,如果取globleLatenessToleranceUseAlternate,并不会得到值null,而是Java会把globleLatenessToleranceUseAlternate进行一个自动开箱拆箱处理。简言之,取得是 globleLatenessToleranceUseAlternate.intValue(),此时,因为globleLatenessToleranceUseAlternate 本身是 null,因此 globleLatenessToleranceUseAlternate.intValue() 导致了空指针因此。 解法 修改如下解决: Integer itemVO = null; Integer globleLatenessToleranceUseAlternate = null; Integer latenessToleranceUseAlternate; if (itemVO != null) { latenessToleranceUseAlternate = itemVO.intValue(); } else { latenessToleranceUseAlternate = globleLatenessToleranceUseAlternate; } 值得注意的是,在新版的JDK和Eclipse中,会做出友好的提示,从而能够有效规避上述问题。提示如下: Null pointer access: This expression of type Integer is null but requires auto-unboxing 参考引用 原文同步至 https://waylau.com/trap-in-java-ternary-expressions/ Java核心编程
JDK 15已经于2020年9月15日如期发布。本文介绍JDK 15新特性。 发布版本说明 根据发布的规划,这次发布的 JDK 15 将是一个短期的过度版,只会被 Oracle 支持(维护)6 个月,直到明年 3 月的 JDK 16 发布此版本将停止维护。而 Oracle 下一个长期支持版(LTS 版)会在明年的 9 月份候发布(Java 17),LTS 版每 3 年发布一个,上一次长期支持版是 18 年 9 月发布的 JDK 11。 下图展示了各个版本的发布历史。 安装包下载 主要分为OpenJDK版本和Oracle版本,下载地址如下: OpenJDK版本:https://jdk.java.net/15/ Oracle版本:http://www.oracle.com/technetwork/java/javase/downloads/index.html 上述版本,如果是个人学习用途,则差异不大。但如果是用于商业用途,则需要仔细看好相关的授权。Oracle JDK根据二进制代码许可协议获得许可,而OpenJDK根据GPL v2许可获得许可。 安装、验证 本例子以OpenJDK版本为例。解压安装包openjdk-15_windows-x64_bin.zip到任意位置。 设置系统环境变量“JAVA_HOME”,如下图所示。 在用户变量“Path”中,增加“%JAVA_HOME%bin”。 安装完成后,执行下面命令进行验证: >java -version openjdk version "15" 2020-09-15 OpenJDK Runtime Environment (build 15+36-1562) OpenJDK 64-Bit Server VM (build 15+36-1562, mixed mode, sharing) 更多有关Java的基本知识,可以参阅《Java核心编程》这本书,描述的非常详细。 JDK 15 新特性说明 JDK 15 为用户提供了14项主要的增强/更改,包括一个孵化器模块,三个预览功能,两个不推荐使用的功能以及两个删除功能。 1. EdDSA 数字签名算法 新加入 Edwards-Curve 数字签名算法(EdDSA)实现加密签名。在许多其它加密库(如 OpenSSL 和 BoringSSL)中得到支持。与 JDK 中的现有签名方案相比,EdDSA 具有更高的安全性和性能。这是一个新的功能。 使用示例如下: // example: generate a key pair and sign KeyPairGenerator kpg = KeyPairGenerator.getInstance("Ed25519"); KeyPair kp = kpg.generateKeyPair(); // algorithm is pure Ed25519 Signature sig = Signature.getInstance("Ed25519"); sig.initSign(kp.getPrivate()); sig.update(msg); byte[] s = sig.sign(); // example: use KeyFactory to contruct a public key KeyFactory kf = KeyFactory.getInstance("EdDSA"); boolean xOdd = ... BigInteger y = ... NamedParameterSpec paramSpec = new NamedParameterSpec("Ed25519"); EdECPublicKeySpec pubSpec = new EdECPublicKeySpec(paramSpec, new EdPoint(xOdd, y)); PublicKey pubKey = kf.generatePublic(pubSpec); 有关EdDSA 数字签名算法的详细内容见RFC 8032规范。 2. 封闭类(预览特性) 可以是封闭类和或者封闭接口,用来增强 Java 编程语言,防止其他类或接口扩展或实现它们。 有了这个特性,意味着以后不是你想继承就继承,想实现就实现了,你得经过允许才行。 示例如下: public abstract sealed class Student permits ZhangSan, LiSi, ZhaoLiu { ... } 类 Student 被 sealed 修饰,说明它是一个封闭类,并且只允许指定的 3 个子类继承。 3. 隐藏类 此功能可帮助需要在运行时生成类的框架。框架生成类需要动态扩展其行为,但是又希望限制对这些类的访问。隐藏类很有用,因为它们只能通过反射访问,而不能从普通字节码访问。此外,隐藏类可以独立于其他类加载,这可以减少框架的内存占用。这是一个新的功能。 4. 移除了 Nashorn JavaScript 脚本引擎 移除了 Nashorn JavaScript 脚本引擎、APIs,以及 jjs 工具。这些早在 JDK 11 中就已经被标记为 deprecated 了,JDK 15 被移除就很正常了。 Nashorn 是 JDK 1.8 引入的一个 JavaScript 脚本引擎,用来取代 Rhino 脚本引擎。Nashorn 是 ECMAScript-262 5.1 的完整实现,增强了 Java 和 JavaScript 的兼容性,并且大大提升了性能。 那么为什么要移除? 官方的解释是主要的:随着 ECMAScript 脚本语言的结构、API 的改编速度越来越快,维护 Nashorn 太有挑战性了,所以……。 5. 重新实现 DatagramSocket API 重新实现旧版 DatagramSocket API,更简单、更现代的实现来代替java.net.DatagramSocket和java.net.MulticastSocketAPI 的基础实现,提高了 JDK 的可维护性和稳定性。 新的底层实现将很容易使用虚拟线程,目前正在 Loom 项目中进行探索。这也是 JEP 353 的后续更新版本,JEP 353 已经重新实现了 Socket API。 6. 准备禁用和废除偏向锁 在 JDK 15 中,默认情况下禁用偏向锁(Biased Locking),并弃用所有相关的命令行选项。 后面再确定是否需要继续支持偏向锁,国为维护这种锁同步优化的成本太高了。 7. 模式匹配(第二次预览) 第一次预览是 JDK 14 中提出来的,点击这里查看我之前写的详细教程。 Java 14 之前用法: if (obj instanceof String) { String s = (String) obj; // 使用s } Java 14之后的用法: if (obj instanceof String s) { // 使用s } Java 15 并没有对此特性进行调整,继续预览特性,只是为了收集更多的用户反馈,可能还不成熟吧。 8. ZGC 功能转正 ZGC是一个可伸缩、低延迟的垃圾回收器。 ZGC 已由JEP 333集成到JDK 11 中,其目标是通过减少 GC 停顿时间来提高性能。借助 JEP 377,JDK 15 将 ZGC 垃圾收集器从预览特性变更为正式特性而已,没错,转正了。 这个 JEP 不会更改默认的 GC,默认仍然是 G1。 9. 文本块功能转正 文本块,是一个多行字符串,它可以避免使用大多数转义符号,自动以可预测的方式格式化字符串,并让开发人员在需要时可以控制格式。 文本块最早准备在 JDK 12 添加的,但最终撤消了,然后在 JDK 13 中作为预览特性进行了添加,然后又在 JDK 14 中再次预览,在 JDK 15 中,文本块终于转正,暂不再做进一步的更改。 Java 13 之前用法,使用one-dimensional的字符串语法: String html = "<html>\n" + " <body>\n" + " <p>Hello, world</p>\n" + " </body>\n" + "</html>\n"; Java 13 之后用法,使用two-dimensional文本块语法: String html = """ <html> <body> <p>Hello, world</p> </body> </html> """; 10. Shenandoah 垃圾回收算法转正 Shenandoah 垃圾回收从实验特性变为产品特性。这是一个从 JDK 12 引入的回收算法,该算法通过与正在运行的 Java 线程同时进行疏散工作来减少 GC 暂停时间。Shenandoah 的暂停时间与堆大小无关,无论堆栈是 200 MB 还是 200 GB,都具有相同的一致暂停时间。 JDK 15 Shenandoah垃圾收集器从预览特性变更为正式特性而已,没错,又是转正了。 11. 移除了 Solaris 和 SPARC 端口。 移除了 Solaris/SPARC、Solaris/x64 和 Linux/SPARC 端口的源代码及构建支持。这些端口在 JDK 14 中就已经被标记为 deprecated 了,JDK 15 被移除也不奇怪。 12. 外部存储器访问 API(二次孵化) 这个最早在 JDK 14 中成为孵化特性,JDK 15 继续二次孵化并对其 API 有了一些更新。 目的是引入一个 API,以允许 Java 程序安全有效地访问 Java 堆之外的外部内存。这同样是 Java 14 的一个预览特性。 13. Records Class(二次预览) Records Class 也是第二次出现的预览功能,它在 JDK 14 中也出现过一次了,使用 Record 可以更方便的创建一个常量类,使用的前后代码对比如下。 旧写法: class Point { private final int x; private final int y; Point(int x, int y) { this.x = x; this.y = y; } int x() { return x; } int y() { return y; } public boolean equals(Object o) { if (!(o instanceof Point)) return false; Point other = (Point) o; return other.x == x && other.y = y; } public int hashCode() { return Objects.hash(x, y); } public String toString() { return String.format("Point[x=%d, y=%d]", x, y); } } 新写法: record Point(int x, int y) { } 也就是说在使用了 record 之后,就可以用一行代码编写出一个常量类,并且这个常量类还包含了构造方法、toString()、equals() 和 hashCode() 等方法。 14. 废除 RMI 激活 废除 RMI 激活,以便在将来进行删除。需要说明的是,RMI 激活是 RMI 中一个过时的组件,自 Java 8 以来一直是可选的。 参考引用 本文同步至: https://waylau.com/jdk-15-released/ https://jdk.java.net/15/release-notes https://openjdk.java.net/projects/jdk/15/ https://openjdk.java.net/projects/jdk/15/spec/ 《Java核心编程》 https://github.com/waylau/modern-java-demos
曾几何时,业界流行使用LAMP架构(Linux、Apache、MySQL和PHP)来快速开发中小网站。LAMP是开放源代码的,而且使用简单、价格廉价,因此LAMP这个组合成为了当时开发中小网站的首选,号称“平民英雄”。而今,随着Node.js的流行,这使得JavaScript终于能够在服务器端拥有了一席之地。JavaScript成为了从前端到后端再到数据库层能够支持全栈开发的语言。而以MongoDB、Express、Angular和Node.js四种开源技术为基础的MEAN架构,除了具备LAMP架构的一切优点外,更能支撑高可用、高并发的大型互联网应用的开发。MEAN架构势必也会成为新的“平民英雄”。 本文介绍了MEAN架构的概念、发展趋势,并阐述了如何学习和使用MEAN架构。 什么是MEAN架构? MEAN架构,是指以MongoDB、Express、Angular和Node.js四种技术为核心的技术栈,广泛应用于全堆栈Web开发。 1. MongoDB MongoDB是强大的非关系型数据库(NoSQL)。与Redis或者HBase等不同,MongoDB是一个介于关系数据库和非关系数据库之间的产品,是非关系数据库当中功能最丰富、最像关系数据库的,旨在为Web应用提供可扩展的高性能数据存储解决方案。它支持的数据结构非常松散,是类似JSON的BSON格式,因此可以存储比较复杂的数据类型。MongoDB最大的特点是其支持的查询语言非常强大,其语法有点类似于面向对象的查询语言,几乎可以实现类似关系数据库单表查询的绝大部分功能,而且还支持对数据建立索引。自MongoDB 4.0开始,MongoDB开始支持事务管理。 图1-1是最新的数据库排行结果。从图中可以看到,MongoDB是在NoSQL数据库中是排行第一的。该数据来自于DB-Engines(https://db-engines.com/en/ranking) [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5FSWZ67e-1591539233123)(https://waylau.com/images/post/20200607-mongodb.png)] 在MEAN架构中,MongoDB承担着数据存储的角色。 2. Express Express是一个简洁而灵活的Node.js Web应用框架, 提供了一系列强大特性帮助你创建各种Web应用。同时,Express也是一款功能非常强大的HTTP工具。 使用Express可以快速地搭建一个完整功能的网站。其核心特性包括: 可以设置中间件来响应HTTP请求。 定义了路由表用于执行不同的HTTP请求动作。 可以通过向模板传递参数来动态渲染HTML页面。 在MEAN架构中,Express承担着构建Web服务的角色。 3. Angular 前端组件化开发是目前主流的开发方式,不管是Angular、React还是Vue.js都如此。相比较而言,Angular不管是其开发功能,还是编程思想,在所有前端框架中都是首屈一指的,特别适合大型企业级应用的开发。 Angular不仅仅是一个前端的框架,而更像是一个前端开发平台,试图解决现代Web应用开发各个方面的问题。Angular有着诸多特性,核心功能包括MVC模式、模块化、自动化双向数据绑定、语义化标签、服务、依赖注入等。而这些概念即便对于后端开发人员来说也不陌生。比如,Java开发人员肯定知道MVC模式、模块化、服务、依赖注入等。 在MEAN架构中,Angular承担着UI客户端开发的角色。 4. Node.js Node.js是整个MEAN架构的基石。Node.js采用事件驱动和非阻塞I/O模型,使其变得轻微和高效,非常适合构建运行在分布式设备的数据密集型实时应用。自从有了Node.js,JavaScript不再只是前端开发的小脚色,而是拥有了从前后台到数据数据库完整开发能力的全栈能手。JavaScript和Node.js是相辅相成的,配合流行的JavaScript语言,使得Node.js拥有更广泛的受众。 Node.js能够火爆的另外一个原因是npm。npm可以轻松管理项目依赖,同时也促进了Node.js生态圈的繁荣,因为npm让开发人员分享开源技术变得不再困难。 MEAN架构的优势 MEAN架构的在企业级应用中被广泛采用,总结起来具备以下优势。 1. 开源 正如前两节所述,无论是MongoDB、Express、Angular、Node.js四种核心技术,还是NG-ZORRO、ngx-markdown、NGINX、basic-auth等周边技术,MEAN架构所有的技术栈都是开源的。 开源技术相对与闭源技术而言,有其优势。一方面,开源技术源码是公开的,互联网公司在考察某项技术是否符合自身开发需求时,可以对源码进分析;另一方面,开源技术相对闭源技术而言,商用的成本相对比较低,这对于很多初创的互联网公司而言,可以节省一大笔技术投入。以此,MEAN架构也被称为开发下一代大型互联网应用的“平民英雄”。 当然,开源技术是把双刃剑,你能够看到源码,并不意味着你可以解决所有问题。开源技术在技术支持上不能与闭源技术相提并论,毕竟闭源技术都有成熟的商业模式,会提供完善的商业支持。而开源技术,更多依赖于社区对于开源技术的支持。如果在使用开源技术过程中发现了问题,可以反馈给开源社区,但开源社区不会给你保证什么时候、什么版本能够修复发现的问题。所以,使用开源技术,需要开发团队对开源技术要有深刻的了解。最好能够吃透源码,这样在发现问题时,能够及时解决源码上的问题。 比如,在关系型数据库方面,同属于Oracle公司的MySQL数据库和Oracle数据库,就是开源与闭源技术的两大代表,两者占据了全球数据库的占有率的前两名。MySQL数据库主要是在中小企业或者是云计算供应商中广泛采用,而Oracle数据库则由于其稳定、高性能的特性,深受政府和银行等客户的信赖。 2. 跨平台 跨平台,意味着开发和部署的应用的成本的降低。 试想一下,当今操作系统三足鼎立,分别是Linux、macOS、Windows。如果开发者需要针对不同的操作系统平台,而要开发不同的软件,那么开发成本势必会非常高。而且每个操作系统平台,都有不同的版本、分支,仅仅做不同的版本的适配都需要耗费极大的人力,更别提要针对不同的平台开发软件了。以此,跨平台可以节省开发成本。 同理,由于MEAN架构开发的软件是具有跨平台的,无需担心在部署应用过程中的兼容性问题。开发者在本地开发环境所开发的软件,理论上是可以通过CICD平台直接一键部署到测试环境,甚至是生产环境中,因而可以节省部署的成本。 MEAN架构的跨平台特性,使其非常适合构建Cloud Native应用,特别是在当今容器技术常常作为微服务的宿主,而MEAN架构的应用是支持Docker部署的。 有关Cloud Native方面的内容,可以参阅笔者所著的《Cloud Native 分布式架构原理与实践》。 3. 全栈开发 类似与系统架构师,全栈开发者应该是比一般的软件工程师具有更广的知识面,是拥有全端软件设计思想并掌握多种开发技能的复合型人才,能狗独当一面。相比于Node.js工程师、Angular工程师偏重于某项技能而言,全栈开发意味着必须掌握整个架构的全部细节,要求全栈开发者能够从零开始构建全套完整的企业级应用。 作为一名全栈开发者,在开发时往往会做如下风险的预测,并做好防御。 当前所开发的应用会部署到什么样的服务器、网络环境中? 服务哪里可能会崩?为什么会崩? 是否应该适当的使用云存储? 程序有无具备数据冗余? 是否具备可用性? 界面是否友好? 性能是否能够满足当前的要求? 哪些位置需要加日志,方便日志排查问题? 除上述的思考外,全栈开发者要能够建立合理的、标准的关系模型,包括外键、索引、视图、查找表等。 全栈开发者要熟悉非关系型数据存储,并且知道它们相对关系型存储优势所在。 当然,人的精力毕竟有限,所以想要成为全栈开发者并非易事。所幸MEAN架构让这一切成为了可能。MEAN架构以Node.js为整个技术栈的核心,而Node.js的编程语言是JavaScript,这意味着,开发者只需要掌握JavaScript这一种编程语言,即可以打通所有MEAN架构的技术,这不得不说是全栈开发者的福音。 4. 支持企业级应用 无论是Node.js、Angular还是MongoDB,这些技术在大型互联网公司都被广泛采用。无数应用也证明了MEAN架构是非常适合构建企业级应用的。企业级应用是指那些为商业组织、大型企业而创建并部署的解决方案及应用。这些大型企业级应用的结构复杂,涉及的外部资源众多、事务密集、数据量大、用户数多,有较强的安全性考虑。 MEAN架构用来开发企业级应用,不但具有强大的功能,还能够满足未来业务需求的变化,且易于升级和维护。 更多有关企业级应用开发方面的内容,可以参阅笔者所著的《Spring Boot 企业级应用开发实战》《Angular企业级应用开发实战》《Node.js企业级应用开发实战》等。 5. 支持构建微服务 微服务(Microservices)架构风格就像是把小的服务开发成单一应用的形式,运行在其自己的进程中,并采用轻量级的机制进行通信(一般是HTTP资源API)。这些服务都是围绕业务能力来构建,通过全自动部署工具来实现独立部署。这些服务,其可以使用不同的编程语言和不同的数据存储技术,并保持最小化集中管理。 MEAN架构非常适合构建微服务: Node.js本身提供了跨平台的能力,可以运行在自己的进程中。 Express易于构建Web服务,并支持HTTP的通信。 Node.js+MongoDB支持从前端到后端再到数据库全栈开发能力。 开发人员可以轻易地通过MEAN架构来构建并快速启动一个微服务应用。业界也提供了成熟的微服务解决方案来打造大型微服务架构系统,比如Tars.js、Seneca等。 读者欲了解更多微服务方面的内容,可以参阅笔者所著的《Spring Cloud 微服务架构开发实战》。 6. 业界主流 MEAN架构所涉及的技术都是业界主流,主要体现在以下几方面。 MongoDB是在NoSQL数据库中是排行第一的,而且用户量还在递增。 只要知道JavaScript就必然知道Node.js,而JavaScript是在开源界最流行的开发语言。 前端组件化开发是目前主流的开发方式,不管是Angular、React还是Vue.js都如此。相比较而言,Angular不管是其开发功能,还是编程思想,在所有前端框架中都是首屈一指的,特别适合大型企业级应用的开发。而且,从市场占有率来看,Angular都是首屈一指的。 在大型互联网应用中,经常使用NGINX作为Web服务器。NGINX也是目前使用最广泛的代理服务器。 如何学习MEAN架构? MEAN架构知识点繁多,涉及面广,不是一时可以掌握。关于MEAN架构,笔者撰写了多本开源书籍,方便网友学习。包括: REST 案例大全 REST 实战 CSS3 教程 跟老卫学Ionic Node.js 案例大全 跟老卫学Angular 这些开源书都免费的,可以随时学习哦,附带案例和源码。有任何问题都可以在线在相关的主页留言,有问必答。 当然,笔者也出版了一些的专著,网友们可以按需选择。包括: Angular企业级应用开发实战 大型互联网应用轻量级架构实战 MongoDB+Express+Angular+Node.js全栈开发实战派 上面的案例和源码也都是公开免费的哦。 参考引用 本文同步至: https://waylau.com/mean-architecture-in-action/ Angular企业级应用开发实战(2019年06月出版) 大型互联网应用轻量级架构实战(2019年12月出版) MongoDB+Express+Angular+Node.js全栈开发实战派(2020年06月出版) Spring Boot 企业级应用开发实战(2018年03月出版) Spring Cloud 微服务架构开发实战(2018年06月出版)
本文介绍了Java的发展趋势,并阐述了如何学习Java技术。 Java为啥火爆? 随着互联网应用的发展,各种编程语言层出不穷,比如C#、Golang、TypeScript、ActionScript等,但不管是哪种语言,都无法撼动Java的“霸主”地位。Java语言始终占据着各类编程语言排行榜的榜首,开发者对于Java的热情也是与日俱增。Java已然成为了企业级应用、云计算和Cloud Native应用的首选语言。 图1-1展示的是1985年至2020年TIOBE编程语言排行榜情况(https://www.tiobe.com/tiobe-index/)。从图中可以看出,自Java诞生以来,一直占据排行版前三的位置。 那么为什么Java一致能保持这么火爆呢?究其原因,笔者认为Java能够长盛不衰的最大的秘诀就是能够与时俱进,不断推陈出新。 笔者从事Java开发已经有十几年了,可以说是Java技术发展的见证者和实践者。为了推广Java技术,笔者撰写了包括《分布式系统常用技术及案例分析》、《Spring Boot 企业级应用开发实战》、《Spring Cloud 微服务架构开发实战》、《Spring 5开发大全》、《Cloud Native 分布式架构原理与实践》等在内了几十本Java领域的专著和开源书,期望以个人微薄之力对Java语言有所贡献。由于目前企业所使用的Java,大多是Java 8之前的版本,市面上也缺乏最新Java 12的学习资料,因此笔者才撰写了这本《Java核心编程》一书以补空白。 Java应该怎么学? 那么,Java应该怎么学?参考《Java核心编程》,学习Java分为以下几个层次: 1. 零基础的读者 如果你是没有任何编程经验的技术爱好者,本书可以帮助你打开编程之门。本书案例丰富、思路清晰,可以由浅及深地帮助读者掌握Java。 同时,本书可以帮助读者从一开始就建立正确的编程习惯,逐步树立良好的面向对象设计思维,这对于学习其他语言都是非常有帮助的。 针对这类读者,建议读者在学习过程中,从头至尾详细跟随笔者来理解Java的概念,并编写《Java核心编程》书中的示例。该书附赠从Java 8到Java 14全套案例:https://github.com/waylau/modern-java 2. 有后端开发经验的读者 对于有后端或者是其他面向对象编程的经验的开发而言,理解并掌握Java并非难事。 针对这类读者,适当理解下Java的语法即可,把精力放在动手编写Java示例上面。 3. 有Java开发经验的读者 大多数Java开发人员肯定熟悉Java的语法,那么需要并把精力放在Java新特性上面,根据自身的实际情况,可以选学本书中的知识点,做到查漏补缺。 让我们一起踏上Java的学习之旅吧。 参考引用 本文同步至: https://waylau.com/java-development-is-still-mainstream/ Eclipse IDE支持Java 14: https://waylau.com/eclipse-ide-support-java14/ 现代Java案例大全,从Java 8到Java 14:https://github.com/waylau/modern-java
本文演示了如何如何编写JUnit 5测试用例,在Maven项目中运行JUnit 5测试用例。 编写JUnit 5测试用例 如果你是Java开发者,那么对于JUnit应该就不陌生。JUnit是Java单元测试的基础工具。 JUnit目前最新的版本是JUnit 5.x,但广大的Java开发者估计还停留在JUnit 4.x,因此有必要演示下如何编写JUnit 5测试用例。 引入JUnit 5依赖 相比较JUnit 4而言,JUnit 5一个比较大的改变是JUnit 5拥有与JUnit 4不同的全新的API。JUnit 5分成了三部分: JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage JUnit Platform是在JVM上启动测试框架的基础。 它还定义了TestEngine API,用于开发在平台上运行的测试框架。 此外,该JUnit Platform还提供了一个控制台启动器(用于从命令行启动该平台)和一个基于JUnit 4的运行器,用于在基于JUnit 4的环境中在该平台上运行任何TestEngine。 流行的IDE(IntelliJ IDEA,Eclipse,NetBeans和Visual Studio Code等)和构建工具(Gradle,Maven和Ant等)中也存在对JUnit平台的一流支持。 JUnit Jupiter是新编程模型和扩展模型的组合,用于在JUnit 5中编写测试和扩展。Jupiter子项目提供了一个TestEngine,用于在平台上运行基于Jupiter的测试。 JUnit Vintage提供了一个TestEngine,用于在平台上运行基于JUnit 3和基于JUnit 4的测试。 因此,在Maven中,JUnit 5分模块的,意味着你可以按需引入上面定义的任意模块。这使得引入JUnit 5依赖就有了多个选择。 一般而言,力求省事,就可以通过引入junit-jupiter依赖。junit-jupiter就是常用JUnit 5模块的聚合包。 <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter</artifactId> <version>${junit-jupiter.version}</version> <scope>test</scope> </dependency> 编写测试用例 下面是一段简单的Java程序: /** * Welcome to https://waylau.com */ package com.waylau.java.demo; /** * Hello World. * * @since 1.0.0 2020年4月12日 * @author <a href="https://waylau.com">Way Lau</a> */ public class HelloWorld { private String words; public HelloWorld(String words) { this.words = words; } public String getWords() { return words; } } 按照管理,我们会在Maven工程的test目录,创建一个与之对应的单元测试用例: /** * Welcome to https://waylau.com */ package com.waylau.java.demo; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; /** * HelloWorld Test. * * @since 1.0.0 2020年4月12日 * @author <a href="https://waylau.com">Way Lau</a> */ class HelloWorldTests { @Test void testGetWords() { var words = "Hello World"; var hello = new HelloWorld(words); assertEquals(words, hello.getWords()); } } 上述用例非常简单,就是想测试下,HelloWorld的getWords方法,是否与预期的一致。这里需要强调的是JUnit 5和JUnit 4的不同点: JUnit 5使用的API是org.junit.jupiter.api.*包下 测试方法(比如上例testGetWords),可以不加public。 运行JUnit 5测试用例 上如上文所讲,在大多数主流的IDE中,都提供了对JUnit 5的支持。因此可以选择在IDE中运行,也可以通过Maven执行测试。 在IDE中运行 以Eclipse IDE为例,右键类或者方法,选择“Run As -> JUnit Test”即可。如下图所示。 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tONJQN3t-1587044607054)(../images/post/20200412-ide.jpg)] 通过Maven执行测试 在Maven中执行测试用例的命令如下: mvn test 如果你执行了上述命令,会得到下面的测试结果 ------------------------------------------------------- T E S T S ------------------------------------------------------- Running com.waylau.java.demo.HelloWorldTests Tests run: 0, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.002 sec Results : Tests run: 0, Failures: 0, Errors: 0, Skipped: 0 [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 1.983 s [INFO] Finished at: 2020-04-12T11:22:16+08:00 [INFO] ------------------------------------------------------------------------ 上面结果没有失败的用例,但同时你也发现了没有成功的用例。因为根本没有执行测试用例。 这是因为,在Maven中并不能直接识别JUnit 5测试用例。如何解决?此时,还需要额外加多Maven Surefire或Maven Failsafe两个插件。 <build> <plugins> <plugin> <artifactId>maven-surefire-plugin</artifactId> <version>${maven-surefire-plugin.version}</version> </plugin> <plugin> <artifactId>maven-failsafe-plugin</artifactId> <version>${maven-failsafe-plugin.version}</version> </plugin> </plugins> </build> 在Maven中再次执行测试用例,会得到下面的测试结果: [INFO] ------------------------------------------------------- [INFO] T E S T S [INFO] ------------------------------------------------------- [INFO] Running com.waylau.java.demo.HelloWorldTests [INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.045 s - in com.waylau.java.demo.HelloWorldTests [INFO] [INFO] Results: [INFO] [INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0 [INFO] [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 4.116 s [INFO] Finished at: 2020-04-12T11:30:29+08:00 [INFO] ------------------------------------------------------------------------ 可以看到,HelloWorldTests类已经得到了测试执行。 参考引用 本文同步至: https://waylau.com/running-junit5-tests-with-maven/ Eclipse IDE支持Java 14: https://waylau.com/eclipse-ide-support-java14/ 本文示例源码:https://github.com/waylau/java-data-structures-and-algorithms-in-action
随着JDK 14的发布(https://waylau.com/jdk-14-released/),各大Java IDE也开始支持JDK 14。最新版本的Eclipse IDE 2020-03也于2020年3月18日发布,本文介绍如何通过Eclipse IDE来开发Java 14应用。 下载 下载地址https://www.eclipse.org/downloads/packages/ 根据个人的需要,下载Java Developers或者Enterprise Java Developers版本。 设置JDK 下载最新的JDK 14,并在Eclipse IDE中指向该JDK。 设置Maven 可选。如果项目是使用Maven管理,则建议下载最新的Maven,并在Eclipse IDE中指向该Maven。 设置网络代理 为了更快的下载插件,需要设置代理。 设置代码样式 可选。设置符合自己需求的代码样式。 设置字符 建议使用UTF-8。 安装支持Java 14的插件 由于当前的Eclipse还未正式支持Java 14,需要额外安装Java 14 Support for Eclipse 2020-03 (4.15)插件,用以支持Java 14。插件地址: https://marketplace.eclipse.org/content/java-14-support-eclipse-2020-03-415 安装完成之后,就可以选择使用JDK 14的编译器,同时启用预览功能。 编程 终于可以愉快的玩耍了Java 14了。 本文所有源码可见https://github.com/waylau/modern-java-demos。 参考引用 本文同步至: https://waylau.com/eclipse-ide-support-java14/ 现代Java案例大全:https://github.com/waylau/modern-java-demos
初次使用TortorliseGit的小伙伴,怕是很难找到删除分支的菜单。本文介绍如何使用TortorliseGit删除分支。 右键项目,点击“Switch/Checkout”菜单 在点击右侧的三个小点 选中要删除的分支,右键分支,可以看到“Delete branch”按钮,点击该按钮就能删除分支。 点击左侧“remotes”,可以用同样的手法,来删除远端的分支。 参考引用 本文同步至: https://waylau.com/tortorlise-git-delete-branch/
JDK 14已经于2020年3月17日如期发布。本文介绍JDK 14特性。 JEP 305: instanceof的模式匹配(预览) 通过对instanceof运算符进行模式匹配来增强Java编程语言。 模式匹配允许程序中的通用逻辑,即从对象中有条件地提取组件,可以更简洁,更安全地表示。 这是JDK 14中的预览语言功能。 动机 几乎每个程序都包含某种逻辑,这些逻辑结合了对表达式是否具有某种类型或结构的测试,然后有条件地提取其状态的组件以进行进一步处理。例如,以下是在Java程序中常见的instanceof-and-cast用法: if (obj instanceof String) { String s = (String) obj; // 使用s } 上述示例中,为了能够安全地将obj转为我们期望的String类型,需要通过instanceof运算符对obj进行类型判断。这里发生了三件事: 测试obj是否是一个String 将obj转换为String 声明新的局部变量s,以便我们可以使用字符串值。 这种模式很简单,并且所有Java程序员都可以理解,但是由于一些原因,它不是最优的。 语法乏味 同时执行类型检测和类型转换并不是必要的 String类型在程序中出现了3次,这混淆了后面更重要的逻辑 重复的代码容易滋生错误 在JDK 14中,上述代码可以改为下面的方式: if (obj instanceof String s) { // 使用s } 这样整个代码看上去更加简洁。 描述 类型测试模式由指定类型的谓词和单个绑定变量组成。在下面的代码中,短语String是类型测试模式: if (obj instanceof String s) { // 使用s } else { // 不能使用s } 如果obj是String的实例,则将其强制转换为String并分配给绑定变量s。绑定变量在if语句的true块中,而不在if语句的false块中。 与局部变量的范围不同,绑定变量的范围由包含的表达式和语句的语义确定。例如,在此代码中: if (!(obj instanceof String s)) { .. s.contains(..) .. } else { .. s.contains(..) .. } true块中的s表示封闭类中的字段,false块中的s表示由instanceof运算符引入的绑定变量。 当if语句的条件变得比单个instanceof更复杂时,绑定变量的范围也会相应地增长。 例如,在此代码中: if (obj instanceof String s && s.length() > 5) {.. s.contains(..) ..} 绑定变量s在&&运算符右侧以及true块中。仅当instanceof成功并分配给s时,才评估右侧。 另一方面,在此代码中: if (obj instanceof String s || s.length() > 5) {.. s.contains(..) ..} 绑定变量s不在||右侧的范围内运算符,也不在true块的范围内。s指的是封闭类中的一个字段。 Joshua Bloch的经典著作Effective Java中有一段代码示例: @Override public boolean equals(Object o) { return (o instanceof CaseInsensitiveString) && ((CaseInsensitiveString) o).s.equalsIgnoreCase(s); } 这段代码可以使用新的语法写成: @Override public boolean equals(Object o) { return (o instanceof CaseInsensitiveString cis) && cis.s.equalsIgnoreCase(s); } 这个特性很有意思,因为它为更为通用的模式匹配打开了大门。模式匹配通过更为简便的语法基于一定的条件来抽取对象的组件,而instanceof刚好是这种情况,它先检查对象类型,然后再调用对象的方法或访问对象的字段。 JEP 343: 打包工具(孵化) 该特性旨在创建一个用于打包独立Java应用程序的工具。 动机 许多Java应用程序需要以一流的方式安装在本机平台上,而不是简单地放置在类路径或模块路径上。对于应用程序开发人员来说,交付简单的JAR文件是不够的。他们必须提供适合本机平台的可安装软件包。这允许以用户熟悉的方式分发,安装和卸载Java应用程序。例如,在Windows上,用户希望能够双击一个软件包来安装他们的软件,然后使用控制面板删除该软件。在macOS上,用户希望能够双击DMG文件并将其应用程序拖到Application文件夹中。 打包工具还可以帮助填补其他技术的空白,例如Java Web Start(已从JDK 11中删除)和pack200(已在JDK 11中弃用,可能在以后的版本中删除)。开发人员可以使用jlink将JDK分解为所需的最小模块集,然后使用打包工具生成一个压缩的、可安装的映像,该映像可以部署到目标计算机。 为了以前满足这些要求,JDK 8分发了一个名为javapackager的打包工具。但是,作为删除JavaFX的一部分,该工具已从JDK 11中删除。 描述 jpackage工具将Java应用程序打包到特定于平台的程序包中,该程序包包含所有必需的依赖项。该应用程序可以作为普通JAR文件的集合或作为模块的集合提供。受支持的特定于平台的软件包格式为: Linux:deb和rpm macOS:pkg和dmg Windows:MSI和EXE 默认情况下,jpackage会以最适合其运行系统的格式生成一个软件包。 以下是基本用法: $ jpackage --name myapp --input lib --main-jar main.jar 用法 1. 基本用法:非模块化应用 假设你有一个由JAR文件组成的应用程序,所有应用程序都位于lib目录下,并且主类在lib/main.jar中。下列命令 $ jpackage --name myapp --input lib --main-jar main.jar 将以本地系统的默认格式打包应用程序,并将生成的打包文件保留在当前目录中。如果main.jar中的MANIFEST.MF文件没有Main-Class属性,我们必须显式地指定主类: $ jpackage --name myapp --input lib --main-jar main.jar --main-class myapp.Main 打包的名称是myapp。要启动该应用程序,启动器将从输入目录复制的每个JAR文件都放在JVM的类路径上。 如果希望生成默认格式以外的软件安装包,可以使用--type选项。例如要在macOS上生成pkg文件(而不是dmg文件),我们可以使用下面的命令: $ jpackage --name myapp --input lib --main-jar main.jar --type pkg 2. 基本用法:模块化应用 如果你有一个模块化应用程序,该应用程序由lib目录中的模块化JAR文件和/或JMOD文件组成,并且主类位于myapp模块中,则下面的命令 $ jpackage --name myapp --module-path lib -m myapp 能够将其打包。如果myapp模块无法识别主类,则必须明确指定: $ jpackage --name myapp --module-path lib -m myapp/myapp.Main JEP 345: G1的NUMA内存分配优化 通过实现可识别NUMA的内存分配,提高大型计算机上的G1性能。 动机 现代的多插槽计算机越来越多地具有非统一的内存访问(non-uniform memory access,NUMA),即内存与每个插槽或内核之间的距离并不相等。插槽之间的内存访问具有不同的性能特征,对更远的插槽的访问通常具有更大的延迟。 并行收集器中通过启动-XX:+UseParallelGC能够感知NUMA,这个功能已经实现了多年了,这有助于提高跨多插槽运行单个JVM的配置的性能。其他HotSpot收集器没有此功能,这意味着他们无法利用这种垂直多路NUMA缩放功能。大型企业应用程序尤其倾向于在多个多插槽上以大堆配置运行,但是它们希望在单个JVM中运行具有可管理性优势。 使用G1收集器的用户越来越多地遇到这种扩展瓶颈。 描述 G1的堆组织为固定大小区域的集合。一个区域通常是一组物理页面,尽管使用大页面(通过 -XX:+UseLargePages)时,多个区域可能组成一个物理页面。 如果指定了+XX:+UseNUMA选项,则在初始化JVM时,区域将平均分布在可用NUMA节点的总数上。 在开始时固定每个区域的NUMA节点有些不灵活,但是可以通过以下增强来缓解。为了为mutator线程分配新的对象,G1可能需要分配一个新的区域。它将通过从NUMA节点中优先选择一个与当前线程绑定的空闲区域来执行此操作,以便将对象保留在新生代的同一NUMA节点上。如果在为变量分配区域的过程中,同一NUMA节点上没有空闲区域,则G1将触发垃圾回收。要评估的另一种想法是,从距离最近的NUMA节点开始,按距离顺序在其他NUMA节点中搜索自由区域。 该特性不会尝试将对象保留在老年代的同一NUMA节点上。 此分配政策中不包括Humongous区。对于这些区,将不做任何特别的事情。 JEP 349: JFR事件流 公开JDK Flight Recorder数据以进行连续监视。 动机 HotSpot VM通过JFR产生的数据点超过500个,但是使用者只能通过解析日志文件的方法使用它们。 用户要想消费这些数据,必须开始一个记录并停止,将内容转储到磁盘上,然后解析记录文件。这对于应用程序分析非常有效,但是监控数据却十分不方便(例如显示动态更新数据的仪表盘)。 与创建记录相关的开销包括: 发出在创建新记录时必须发生的事件 写入事件元数据(例如字段布局) 写入检查点数据(例如堆栈跟踪) 将数据从磁盘存储复制到单独的记录文件 如果有一种方法,可以在不创建新记录文件的情况下,从磁盘存储库中读取正在记录的数据,就可以避免上述开销。 描述 jdk.jfr模块里的jdk.jfr.consumer包,提供了异步订阅事件的功能。用户可以直接从磁盘存储库读取记录数据,也可以直接从磁盘存储流中读取数据,而无需转储记录文件。可以通过注册处理器(例如lambda函数)与流交互,从而对事件的到达进行响应。 下面的例子打印CPU的总体使用率,并持有锁10毫秒。 try (var rs = new RecordingStream()) { rs.enable("jdk.CPULoad").withPeriod(Duration.ofSeconds(1)); rs.enable("jdk.JavaMonitorEnter").withThreshold(Duration.ofMillis(10)); rs.onEvent("jdk.CPULoad", event -> { System.out.println(event.getFloat("machineTotal")); }); rs.onEvent("jdk.JavaMonitorEnter", event -> { System.out.println(event.getClass("monitorClass")); }); rs.start(); } RecordingStream类实现了接口jdk.jfr.consumer.EventStream,该接口提供了一种统一的方式来过滤和使用事件,无论源是实时流还是磁盘上的文件。 public interface EventStream extends AutoCloseable { public static EventStream openRepository(); public static EventStream openRepository(Path directory); public static EventStream openFile(Path file); void setStartTime(Instant startTime); void setEndTime(Instant endTime); void setOrdered(boolean ordered); void setReuse(boolean reuse); void onEvent(Consumer<RecordedEvent> handler); void onEvent(String eventName, Consumer<RecordedEvent handler); void onFlush(Runnable handler); void onClose(Runnable handler); void onError(Runnable handler); void remove(Object handler); void start(); void startAsync(); void awaitTermination(); void awaitTermination(Duration duration); void close(); } 创建流的方法有3种: EventStream::openRepository(Path)从磁盘存储库中构造一个流。这是一种可以直接通过文件系统监视其他进程的方法。磁盘存储库的位置存储在系统属性jdk.jfr.repository中,可以使用API读取到。 EventStream::openRepository()方法执行进程内监控。与RecordingStream不同,它不会开始录制。相反,仅当通过外部方式(例如,使用JCMD或JMX)启动记录时,流才接收事件。 EventStream::openFile(Path)从记录文件中创建流,扩充了已经存在的RecordingFile类。 该接口还可用于设置缓冲的数据量,以及是否应按时间顺序对事件进行排序。为了最大程度地降低分配压力,还可以选择控制是否应为每个事件分配新的事件对象,或者是否可以重用以前的对象。我们可以在当前线程中启动流,也可以异步启动流。 JVM每秒一次将线程本地缓冲区中存储的事件定期刷新到磁盘存储库。 一个单独的线程解析最近的文件,直到写入数据为止,然后将事件推送给订阅者。 为了保持较低的开销,仅从文件中读取活动订阅的事件。 要在刷新完成后收到通知,可以使用EventStream::onFlush(Runnable)方法注册处理程序。 这是在JVM准备下一组事件时将数据聚合或推送到外部系统的机会。 JEP 352: 非易失性映射字节缓冲区 添加新的特定于JDK的文件映射模式,以便可以使用FileChannel API创建引用非易失性内存(non-volatile memory,NVM)的MappedByteBuffer实例。 动机 NVM为应用程序程序员提供了在程序运行过程中创建和更新程序状态的机会,而减少了输出到持久性介质或从持久性介质输入时的成本。这对于事务程序特别重要,在事务程序中,需要定期保持不确定状态以启用崩溃恢复。 现有的C库(例如Intel的libpmem)为C程序提供了对基层NVM的高效访问。他们还以此为基础来支持对各种持久性数据类型的简单管理。当前,由于频繁需要进行系统调用或JNI调用来调用原始操作,从而确保内存更改是持久的,因此即使仅使用Java中的基础库也很昂贵。同样的问题限制了高级库的使用,并且由于C中提供的持久数据类型分配在无法从Java直接访问的内存中这一事实而加剧了这一问题。与C或可以低成本链接到C库的语言相比,这使Java应用程序和中间件(例如Java事务管理器)处于严重的劣势。 该特性试图通过允许映射到ByteBuffer的NVM的有效写回来解决第一个问题。由于Java可以直接访问ByteBuffer映射的内存,因此这可以通过实现与C语言中提供的客户端库等效的客户端库来解决第二个问题,以管理不同持久数据类型的存储。 描述 1. 初步变更 该JEP使用了Java SE API的两个增强功能: 支持implementation-defined的映射模式 MppedByteBuffer::force方法以指定范围 2. 特定于JDK的API更改 通过新模块中的公共API公开新的MapMode枚举值 一个公共扩展枚举ExtendedMapMode将添加到jdk.nio.mapmode程序包: package jdk.nio.mapmode; . . . public class ExtendedMapMode { private ExtendedMapMode() { } public static final MapMode READ_ONLY_SYNC = . . . public static final MapMode READ_WRITE_SYNC = . . . } 在调用FileChannel::map方法创建映射到NVM设备文件上的只读或读写MappedByteBuffer时,可以使用上述的枚举值。如果这些标志在不支持NVM设备文件的平台上传递,程序会抛出UnsupportedOperationException异常。在受支持的平台上,仅当目标FileChannel实例是从通过NVM设备打开的派生文件时,才能传递这些参数。在任何其他情况下,都会抛出IOException异常。 发布BufferPoolMXBean,用于跟踪MappedByteBuffer统计信息 JEP 358: 友好的空指针异常 精确描述哪个变量为null,提高JVM生成的NullPointerException的可用性。 动机 每个Java开发人员都遇到过NullPointerException(NPE)问题。NPE几乎可以出现在程序的任意位置,因此尝试捕获和修复它们是不可能的。下面的代码: a.i = 99; JVM会打印出方法名、文件名和NPE异常的行数: Exception in thread "main" java.lang.NullPointerException at Prog.main(Prog.java:5) 使用这个错误报告,开发人员可以定位到a.i = 99;并推断对象a是null。但是对于更复杂的代码,不使用调试器就无法确定哪个变量为空。假设下面的代码中出现了一个NPE: a.b.c.i = 99; 仅仅使用文件名和行数,并不能精确定位到哪个变量为null,是a、b还是c? 访问数组也会发生类似的问题。假设此代码中出现一个NPE: a[i][j][k] = 99; 文件名和行号不能精确指出哪个数组组件为空。是a还是a[i]或a[i][j]? 一行代码可能包含多个访问路径,每个访问路径都可能是NPE的来源。假设此代码中出现一个NPE: a.i = b.j; 文件名和行号并不能确定哪个对象为空,是a还是b? NPE也可能在方法调用中传递,看下面的代码: x().y().i = 99; 文件名和行号不能指出哪个方法调用返回null。是x()还是y()? 描述 JVM在程序调用空引用的位置抛出NPE异常,通过分析程序的字节码指令,JVM可以精确判断哪个变量为空,并在NPE中描述详细信息(根据源代码)。包含方法名、文件名和行号的null-detail消息将显示在JVM的消息中。 例如a.i = 99;的NPE异常可能是如下格式: Exception in thread "main" java.lang.NullPointerException: Cannot assign field "i" because "a" is null at Prog.main(Prog.java:5) 在更复杂的a.b.c.i = 99;语句中,NPE消息会包含导致空值的完整访问路径: Exception in thread "main" java.lang.NullPointerException: Cannot read field "c" because "a.b" is null at Prog.main(Prog.java:5) 同样,如果数组访问和赋值语句a[i][j][k] = 99;引发NPE: Exception in thread "main" java.lang.NullPointerException: Cannot load from object array because "a[i][j]" is null at Prog.main(Prog.java:5) 类似地,a.i = b.j;会引发NPE: Exception in thread "main" java.lang.NullPointerException: Cannot read field "j" because "b" is null at Prog.main(Prog.java:5) JEP 359: record(预览) 通过record增强Java编程语言。record提供了一种紧凑的语法来声明类,这些类是浅层不可变数据的透明持有者。 动机 我们经常听到这样的抱怨:“Java太冗长”、“Java规则过多”。首当其冲的就是充当简单集合的“数据载体”的类。为了写一个数据类,开发人员必须编写许多低价值、重复且容易出错的代码:构造函数、访问器、equals()、hashCode()和toString()等等。 尽管IDE可以帮助开发人员编写数据载体类的绝大多数编码,但是这些代码仍然冗长。 从表面上看,将Record是为了简化模板编码而生的,但是它还有“远大”的目标:modeling data as data(将数据建模为数据)。record应该更简单、简洁、数据不可变。 描述 record是Java的一种新的类型。同枚举一样,record也是对类的一种限制。record放弃了类通常享有的特性:将API和表示解耦。但是作为回报,record使数据类变得非常简洁。 一个record具有名称和状态描述。状态描述声明了record的组成部分。例如: record Point(int x, int y) { } 因为record在语义上是数据的简单透明持有者,所以记录会自动获取很多标准成员: 状态声明中的每个成员,都有一个 private final的字段; 状态声明中的每个组件的公共读取访问方法,该方法和组件具有相同的名字; 一个公共的构造函数,其签名与状态声明相同; equals和hashCode的实现; toString的实现。 限制 records不能扩展任何类,并且不能声明私有字段以外的实例字段。声明的任何其他字段都必须是静态的。 records类都是隐含的final类,并且不能是抽象类。这些限制使得records的API仅由其状态描述定义,并且以后不能被其他类实现或继承。 在record中额外声明变量 也可以显式声明从状态描述自动派生的任何成员。可以在没有正式参数列表的情况下声明构造函数(这种情况下,假定与状态描述相同),并且在正常构造函数主体正常完成时调用隐式初始化(this.x=x)。这样就可以在显式构造函数中仅执行其参数的验证等逻辑,并省略字段的初始化,例如: record Range(int lo, int hi) { public Range { if (lo > hi) /* referring here to the implicit constructor parameters */ throw new IllegalArgumentException(String.format("(%d,%d)", lo, hi)); } } JEP 361: Switch Expressions (标准) 扩展switch可以使其应用于语句或表达式。扩展switch使其可以用作语句或表达式,以便两种形式都可以使用传统的“case ... :”标签(带有贯穿)或“... ->”标签(不带有贯穿),还有另一个新语句,用于从switch表达式产生值。这些更改将简化日常编码,并为在交换机中使用模式匹配提供了方法。这些功能在JDK 12和JDK 13中是属于预览语言功能,在JDK 14中正式称为标准。 动机 当我们准备增强Java编程语言以支持模式匹配(JEP 305)时,现有switch语句的一些不规则性(长期以来一直困扰着用户)成为了障碍。下面的代码中,众多的break语句使代码变得冗长,这种“视觉噪声”通常掩盖了更多的错误。 switch (day) { case MONDAY: case FRIDAY: case SUNDAY: System.out.println(6); break; case TUESDAY: System.out.println(7); break; case THURSDAY: case SATURDAY: System.out.println(8); break; case WEDNESDAY: System.out.println(9); break; } 我们建议引入一种新形式的switch标签“case L ->”,以表示如果匹配标签,则只执行标签右边的代码。switch标签允许在每种情况下使用逗号分隔多个常量。现在可以这样编写以前的代码: switch (day) { case MONDAY, FRIDAY, SUNDAY -> System.out.println(6); case TUESDAY -> System.out.println(7); case THURSDAY, SATURDAY -> System.out.println(8); case WEDNESDAY -> System.out.println(9); } switch标签“case L ->”右侧的代码被限制为表达式、代码块或throw语句。这样局部变量的范围在本块之内,而传统的switch语句局部变量的作用域是整个模块! switch (day) { case MONDAY: case TUESDAY: int temp = ... // The scope of 'temp' continues to the } break; case WEDNESDAY: case THURSDAY: int temp2 = ... // Can't call this variable 'temp' break; default: int temp3 = ... // Can't call this variable 'temp' } 许多现有的switch语句实质上是对switch表达式的模拟,其中每个分支要么分配给一个公共目标变量,要么返回一个值: int numLetters; switch (day) { case MONDAY: case FRIDAY: case SUNDAY: numLetters = 6; break; case TUESDAY: numLetters = 7; break; case THURSDAY: case SATURDAY: numLetters = 8; break; case WEDNESDAY: numLetters = 9; break; default: throw new IllegalStateException("Wat: " + day); } 上面的表述是复杂、重复且容易出错的。代码设计者的意图是为每天计算numLetters。这段代码可以改写成下面这段形式,更加清晰和安全: int numLetters = switch (day) { case MONDAY, FRIDAY, SUNDAY -> 6; case TUESDAY -> 7; case THURSDAY, SATURDAY -> 8; case WEDNESDAY -> 9; }; 描述 1. “case L ->”标签 除了传统的“case L :”标签外,还定义了一种更简洁的形式:“case L ->”标签。如果表达式匹配了某个标签,则仅执行箭头右侧的表达式或语句;否则将不执行任何操作。 static void howMany(int k) { switch (k) { case 1 -> System.out.println("one"); case 2 -> System.out.println("two"); default -> System.out.println("many"); } } 下面的代码: howMany(1); howMany(2); howMany(3); 将会打印: one two many 2. Switch表达式 JDK 14扩展了switch语句,使其可以应用于表达式中。例如上述的howMany方法可以重写为如下形式: static void howMany(int k) { System.out.println( switch (k) { case 1 -> "one"; case 2 -> "two"; default -> "many"; } ); } 在通常情况下,switch表达式如下所示: T result = switch (arg) { case L1 -> e1; case L2 -> e2; default -> e3; }; 3. 通过yield产生值 大多数switch表达式在“case L->”标签的右侧都有一个表达式。如果需要一个完整的块,JDK 14引入了一个新的yield语句来产生一个值,该值成为封闭的switch表达式的值。 int j = switch (day) { case MONDAY -> 0; case TUESDAY -> 1; default -> { int k = day.toString().length(); int result = f(k); yield result; } }; JEP 362: 弃用Solaris和SPARC端口 不建议使用Solaris/SPARC,Solaris/x64和Linux/SPARC端口,以在将来的发行版中删除它们。 动机 放弃对这些端口的支持将使OpenJDK社区中的贡献者能够加速新功能的开发,这些新功能将推动平台向前发展。 JEP 363: 移除CMS垃圾收集器 移除CMS(Concurrent Mark Sweep)垃圾收集器。 动机 在两年多以前的JEP 291中,就已经弃用了CMS收集器,并说明会在以后的发行版中删除,以加快其他垃圾收集器的发展。在这段时间里,我们看到了2个新的垃圾收集器ZGC和Shenandoah的诞生,同时对G1的进一步改进。G1自JDK 6开始便成为CMS的继任者。我们希望以后现有的收集器进一步减少对CMS的需求。 描述 此更改将禁用CMS的编译,删除源代码中gc/cms目录的内容,并删除仅与CMS有关的选项。尝试使用命令-XX:+UseConcMarkSweepGC开启CMS会收到以下警告: Java HotSpot(TM) 64-Bit Server VM warning: Ignoring option UseConcMarkSweepGC; \ support was removed in <version> VM将使用默认收集器继续执行。 JEP 364: macOS系统上的ZGC(实验) 将ZGC垃圾收集器移植到macOS。 动机 尽管我们希望需要ZGC可伸缩性的用户使用基于Linux的环境,但是在部署应用程序之前,开发人员通常会使用Mac进行本地开发和测试。 还有一些用户希望运行桌面应用程序,例如带有ZGC的IDE。 描述 ZGC的macOS实现由两部分组成: 支持macOS上的多映射内存。 ZGC设计大量使用彩色指针,因此在macOS上我们需要一种将多个虚拟地址(在算法中包含不同颜色)映射到同一物理内存的方法。我们将为此使用mach microkernel mach_vm_remap API。堆的物理内存在单独的地址视图中维护,在概念上类似于文件描述符,但位于(主要是)连续的虚拟地址中。该内存被重新映射到内存的各种ZGC视图中,代表了算法的不同指针颜色。 ZGC支持不连续的内存保留。在Linux上,我们在初始化期间保留16TB的虚拟地址空间。我们假设没有共享库将映射到所需的地址空间。在默认的Linux配置上,这是一个安全的假设。但是在macOS上,ASLR机制会侵入我们的地址空间,因此ZGC必须允许堆保留不连续。假设VM实现使用单个连续的内存预留,则共享的VM代码也必须停止。如此一来,is_in_reserved(),reserved_region()和base()之类的GC API将从CollectedHeap中删除。 JEP 365: Windows系统上的ZGC(实验) 将ZGC垃圾收集器移植到Windows系统上。 描述 ZGC的大多数代码库都是平台无关的,不需要Windows特定的更改。现有的x64负载屏障支持与操作系统无关,也可以在Windows上使用。需要移植的特定于平台的代码与如何保留地址空间以及如何将物理内存映射到保留的地址空间有关。用于内存管理的Windows API与POSIX API不同,并且在某些方面不太灵活。 Windows实现的ZGC需要进行以下工作: 支持多映射内存。 ZGC使用彩色指针需要支持堆多重映射,以便可以从进程地址空间中的多个不同位置访问同一物理内存。在Windows上,分页文件支持的内存为物理内存提供了一个标识(句柄),该标识与映射它的虚拟地址无关。使用此标识,ZGC可以将同一物理内存映射到多个位置。 支持将分页文件支持的内存映射到保留的地址空间。 Windows内存管理API不如POSIX的mmap/munmap灵活,尤其是在将文件支持的内存映射到以前保留的地址空间区域中时。为此,ZGC将使用Windows概念的地址空间占位符。 Windows 10和Windows Server版本1803中引入了占位符概念。不会实现对Windows较早版本的ZGC支持。 支持映射和取消映射堆的任意部分。 ZGC的堆布局与其动态调整堆页面大小(以及重新调整大小)相结合,需要支持映射和取消映射任意堆粒子。此要求与Windows地址空间占位符结合使用时,需要特别注意,因为占位符必须由程序显式拆分/合并,而不是由操作系统自动拆分/合并(如在Linux上)。 支持提交和取消提交堆的任意部分。 ZGC可以在Java程序运行时动态地提交和取消提交物理内存。为了支持这些操作,物理内存将被划分为多个分页文件段并由其支持。每个分页文件段都对应一个ZGC堆粒度,并且可以独立于其他段进行提交和取消提交。 JEP 366: 弃用Parallel Scavenge和Serial Old垃圾收集算法的组合 弃用Parallel Scavenge和Serial Old垃圾收集算法的组合。 动机 有一组GC算法的组合很少使用,但是维护起来却需要巨大的工作量:并行年轻代GC(ParallelScavenge)和串行老年代GC(SerialOld)的组合。用户必须使用-XX:+UseParallelGC -XX:-UseParallelOldGC来启用此组合。 这种组合是畸形的,因为它将并行的年轻代GC算法和串行的老年代GC算法组合在一起使用。我们认为这种组合仅在年轻代很多、老年代很少时才有效果。在这种情况下,由于老年代的体积较小,因此完整的收集暂停时间是可以接受的。但是在生产环境中,这种方式是非常冒险的:年轻代的对象容易导致OutOfMemoryException。此组合的唯一优势是总内存使用量略低。我们认为,这种较小的内存占用优势(最多是Java堆大小的约3%)不足以超过维护此GC组合的成本。 描述 除了弃用选项组合-XX:+UseParallelGC -XX:-UseParallelOldGC外,我们还将弃用选项-XX:UseParallelOldGC,因为它唯一的用途是取消选择并行的旧版GC,从而启用串行旧版GC。 因此,任何对UseParallelOldGC选项的明确使用都会显示弃用警告。 JEP 367: 移除Pack200工具和API 删除java.util.jar软件包中的pack200和unpack200工具以及Pack200 API。这些工具和API在Java SE 11中已经被注明为不推荐,并明确打算在将来的版本中删除它们。 动机 Pack200是JSR 200在Java SE 5.0中引入的一种JAR文件压缩方案。其目标是“减少Java应用程序打包,传输和交付的磁盘和带宽需求”。开发人员使用一对工具pack200和unpack200压缩和解压缩其JAR文件。在java.util.jar包中提供了一个API。 删除Pack200的三个原因: 从历史上看,通过56k调制解调器缓慢下载JDK阻碍了Java的采用。 JDK功能的不断增长导致下载量膨胀,进一步阻碍了采用。使用Pack200压缩JDK是缓解此问题的一种方法。但是,时间已经过去了:下载速度得到了提高,并且JDK 9为Java运行时(JEP 220)和用于构建运行时的模块(JMOD)引入了新的压缩方案。因此,JDK 9和更高版本不依赖Pack200。 JDK 8是在构建时用pack200压缩的最新版本,在安装时用unpack200压缩的最新版本。总之,Pack200的主要使用者(JDK本身)不再需要它。 除了JDK,Pack200还可以压缩客户端应用程序,尤其是applet。某些部署技术(例如Oracle的浏览器插件)会自动解压缩applet JAR。但是,客户端应用程序的格局已经改变,并且大多数浏览器都放弃了对插件的支持。因此,Pack200的主要消费者类别(在浏览器中运行的小程序)不再是将Pack200包含在JDK中的驱动程序。 Pack200是一项复杂而精致的技术。它的文件格式与类文件格式和JAR文件格式紧密相关,二者均以JSR 200所无法预料的方式发展。(例如,JEP 309向类文件格式添加了一种新的常量池条目,并且JEP 238在JAR文件格式中添加了版本控制元数据。)JDK中的实现是在Java和本机代码之间划分的,这使得维护变得很困难。 java.util.jar.Pack200中的API不利于Java SE平台的模块化,从而导致在Java SE 9中删除了其四种方法。总的来说,维护Pack200的成本是巨大的,并且超过了其收益。包括在Java SE和JDK中。 描述 JDK最终针对的JDK功能发行版中将删除以前用@Deprecated(forRemoval = true)注解的java.base模块中的三种类型: java.util.jar.Pack200 java.util.jar.Pack200.Packer java.util.jar.Pack200.Unpacker 包含pack200和unpack200工具的jdk.pack模块先前已使用@Deprecated(forRemoval = true)进行了注解,并且还将在此JEP最终针对的JDK功能版本中将其删除。 JEP 368: Text Blocks(二次预览) Java语言增加文本块功能。文本块是多行字符串文字,能避免大多数转义。 动机 在Java中,HTML, XML, SQL, JSON等字符串对象都很难阅读和维护。 1. HTML 使用one-dimensional的字符串语法: String html = "<html>\n" + " <body>\n" + " <p>Hello, world</p>\n" + " </body>\n" + "</html>\n"; 使用two-dimensional文本块语法: String html = """ <html> <body> <p>Hello, world</p> </body> </html> """; 2. SQL 使用one-dimensional的字符串语法: String query = "SELECT `EMP_ID`, `LAST_NAME` FROM `EMPLOYEE_TB`\n" + "WHERE `CITY` = 'INDIANAPOLIS'\n" + "ORDER BY `EMP_ID`, `LAST_NAME`;\n"; 使用two-dimensional文本块语法: String query = """ SELECT `EMP_ID`, `LAST_NAME` FROM `EMPLOYEE_TB` WHERE `CITY` = 'INDIANAPOLIS' ORDER BY `EMP_ID`, `LAST_NAME`; """; 3. 多语言示例 使用one-dimensional的字符串语法: ScriptEngine engine = new ScriptEngineManager().getEngineByName("js"); Object obj = engine.eval("function hello() {\n" + " print('\"Hello, world\"');\n" + "}\n" + "\n" + "hello();\n"); 使用two-dimensional文本块语法: ScriptEngine engine = new ScriptEngineManager().getEngineByName("js"); Object obj = engine.eval(""" function hello() { print('"Hello, world"'); } hello(); """); 描述 文本块是Java语言的新语法,可以用来表示任何字符串,具有更高的表达能力和更少的复杂度。 文本块的开头定界符是由三个双引号字符(""")组成的序列,后面跟0个或多个空格,最后跟一个行终止符。内容从开头定界符的行终止符之后的第一个字符开始。 结束定界符是三个双引号字符的序列。内容在结束定界符的第一个双引号之前的最后一个字符处结束。 与字符串文字中的字符不同,文本块的内容中可以直接包含双引号字符。允许在文本块中使用\",但不是必需的或不建议使用。 与字符串文字中的字符不同,内容可以直接包含行终止符。允许在文本块中使用\n,但不是必需或不建议使用。例如,文本块: """ line 1 line 2 line 3 """ 等效于字符串文字: "line 1\nline 2\nline 3\n" 或字符串文字的串联: "line 1\n" + "line 2\n" + "line 3\n" JEP 370: 外部存储器API(孵化) 引入一个API,以允许Java程序安全有效地访问Java堆之外的外部内存。 动机 许多Java的库都能访问外部存储,例如Ignite、mapDB、memcached及Netty的ByteBuf API。这样可以: 避免垃圾回收相关成本和不可预测性 跨多个进程共享内存 通过将文件映射到内存中来序列化、反序列化内存内容。 但是Java API却没有提供一个令人满意的访问外部内存的解决方案。 Java 1.4中引入的ByteBuffer API允许创建直接字节缓冲区,这些缓冲区是按堆外分配的,并允许用户直接从Java操作堆外内存。但是,直接缓冲区是有限的。 开发人员可以从Java代码访问外部内存的另一种常见途径是使用sun.misc.Unsafe API。Unsafe有许多公开的内存访问操作(例如Unsafe::getInt和putInt)。使用Unsafe访问内存非常高效:所有内存访问操作都定义为JVM内在函数,因此JIT会定期优化内存访问操作。然而根据定义,Unsafe API是不安全的——它允许访问任何内存位置(例如,Unsafe::getInt需要很长的地址)。如果Java程序了访问某些已释放的内存位置,可能会使JVM崩溃。最重要的是,Unsafe API不是受支持的Java API,并且强烈建议不要使用它。 虽然也可以使用JNI访问内存,但是与该解决方案相关的固有成本使其在实践中很少适用。整个开发流程很复杂,因为JNI要求开发人员编写和维护C代码段。 JNI本质上也很慢,因为每次访问都需要Java到native的转换。 在访问外部内存时,开发人员面临一个难题:应该使用安全但受限(可能效率较低)的方法(例如ByteBuffer),还是应该放弃安全保证并接受不受支持和危险的Unsafe API? 该JEP引入了受支持的,安全且有效的外部内存访问API。并且设计时就充分考虑了JIT优化。 描述 外部存储器访问API引入了三个主要的抽象:MemorySegment,MemoryAddress和MemoryLayout。 MemorySegment用于对具有给定空间和时间范围的连续内存区域进行建模。可以将MemoryAddress视为段内的偏移量,MemoryLayout是内存段内容的程序描述。 可以从多种来源创建内存段,例如本机内存缓冲区,Java数组和字节缓冲区(直接或基于堆)。例如,可以如下创建本机内存段: try (MemorySegment segment = MemorySegment.allocateNative(100)) { ... } 上述代码将创建大小为100字节的,与本机内存缓冲区关联的内存段。 内存段在空间上受限制;任何试图使用该段来访问这些界限之外的内存的尝试都会导致异常。正如使用try-with-resource构造所证明的那样,片段在时间上也是有界的。也就是说,它们已创建,使用并在不再使用时关闭。关闭段始终是一个显式操作,并且可能导致其他副作用,例如与该段关联的内存的重新分配。任何访问已关闭的内存段的尝试都将导致异常。空间和时间安全性检查对于确保内存访问API的安全性至关重要。 通过获取内存访问var句柄可以取消引用与段关联的内存。这些特殊的var句柄具有至少一个强制访问坐标,类型为MemoryAddress,即发生取消引用的地址。它们是使用MemoryHandles类中的工厂方法获得的。要设置本机段的元素,我们可以使用如下所示的内存访问var句柄: VarHandle intHandle = MemoryHandles.varHandle(int.class); try (MemorySegment segment = MemorySegment.allocateNative(100)) { MemoryAddress base = segment.baseAddress(); for (int i = 0 ; i < 25 ; i++) { intHandle.set(base.offset(i * 4), i); } } 参考引用 本文同步至: https://waylau.com/jdk-14-released/ https://openjdk.java.net/projects/jdk/14/
在前文,我们介绍来了分布式事务,以及分布式事务的解决方案之一的二阶段提交。本文介绍分布式事务处理方案之一的三阶段提交协议。 分布式事务 分布式事务是指发生在多个数据节点之间的事务,分布式事务比单机事务要复杂的多。在分布式系统中,各个节点之间在是相互独立的,需要通过网络进行沟通和协调。由于存在事务机制,可以保证每个独立节点上的数据操作可以满足ACID。但是,相互独立的节点之间无法准确地知道其他节点的事务执行情况。所以从理论上来讲,两个节点的数据是无法达到一致的状态。如果想让分布式部署的多个节点中的数据保持一致性,那么就要保证在所有节点数据的写操作,要么全部都执行,要么全部都不执行。但是,一台机器在执行本地事务的时候无法知道其他机器中的本地事务的执行结果,所以它也就不知道本次事务到底应该commit还是rollback。所以,常规的解决办法就是引入一个"协调者"的组件来统一调度所有分布式节点的执行。 为了解决这种分布式一致性问题,前人在性能和数据一致性的反反复复权衡过程中总结了许多典型的协议和算法。其中比较著名的有二阶提交协议(Two Phase Commitment Protocol)、三阶提交协议(Three Phase Commitment Protocol)和Paxos算法。针对分布式事务,是X/Open 这个组织定义的一套分布式事务的标准X/Open DTP(X/Open Distributed Transaction Processing ReferenceModel),定义了规范和API接口,可以由各个厂商进行具体的实现。 大部分的关系型数据库通过两阶段提交(Two Phase Commit,2PC)算法来完成分布式事务,比如Oracle中通过dblink方式进行事务处理。下面重点介绍下3PC算法。 下面重点介绍下三阶提交协议算法。 三阶段提交概述 三阶段提交协议可以理解为两阶段提交协议的改良版,是在协调者和参与者中都引入超时机制,并且把两阶段提交协议的第一个阶段分成了两步: 询问,然后再锁资源,最后真正提交。 两阶段提交协议最早是分布式事务的专家Jim Gray在1978年的一篇文章Notes on Database Operating Systems中提及。两阶段提交协议可以保证数据的强一致性,即保证了分布式事务的原子性:所有结点要么全做要么全不做。许多分布式关系型数据管理系统采用此协议来完成分布式事务。它是协调所有分布式原子事务参与者,并决定提交或取消(回滚)的分布式算法。同时也是解决一致性问题的算法。该算法能够解决很多的临时性系统故障(包括进程、网络节点、通信等故障),被广泛地使用。但是,它并不能够通过配置来解决所有的故障,在某些情况下它还需要人为的参与才能解决问题。两阶段提交协议存在的问题是,协调者在某些时刻如果失败了, 整个事务就会阻塞。于是Skeen发布了"NonBlocking Commit Protocols" (1981)这篇论文,论文指出在一个分布式的事务里面, 需要一个三阶段的提交协议来避免在两阶段提交中存在的阻塞问题。 顾名思义,三阶段提交分为以下三个阶段: CanCommit PreCommit DoCommit 在三阶段提交协议中,系统一般包含两类角色: 协调者(Coordinator),通常一个系统中只有一个; 参与者(Participant),一般包含多个,在数据存储系统中可以理解为数据副本的个数。 CanCommit 在CanCommit阶段,协调者协议流程如下: 写本地日志“BEGIN_COMMIT”,并进入WAIT状态; 向所有参与者发送“VOTE_REQUEST”消息; 等待并接收参与者发送的对“VOTE_REQUEST”的响应。参与者响应“VOTE_ABORT”或“VOTE_COMMIT”消息给协调者。 该流程与两阶段提交协议类似。 PreCommit 在PreCommit阶段,,协调者将通知事务参与者准备提交或取消事务,写本地的redo和undo日志,但不提交。 协调者协议流程如下: 若收到任何一个参与者发送的“VOTE_ABORT”消息; 写本地“GLOBAL_ABORT”日志,进入ABORT状态; 向所有的参与者发送“GLOBAL_ABORT”消息; 若收到所有参与者发送的“VOTE_COMMIT”消息; 写本地“PREPARE_COMMIT”日志,进入PRECOMMIT状态; 向所有的参与者发送“PREPARE _COMMIT”消息; 等待并接收参与者发送的对“GLOBAL_ABORT”消息或“PREPARE_COMMIT”消息的确认响应消息。一旦收到所有参与者的“GLOBAL_ABORT”确认消息或者超时没有收到,写本地“END_TRANSACTION”日志流程结束,则不再进入DoCommit阶段。如果收到所有参与者的“PREPARE_COMMIT”确认消息,则进入DoCommit阶段。 该流程与两阶段提交协议相比,多了一个PRECOMMIT状态。 DoCommit 在该阶段, 协调者协议流程如下: 向所有参与者发送的“GLOBAL _COMMIT”消息; 等待并接收参与者发送的对 “GLOBAL_COMMIT”消息的确认响应消息,一旦收到所有参与者的确认消息,写本地“END_TRANSACTION”日志流程结束。 在DoCommit阶段,如果参与者无法及时接收到来自协调者的GLOBAL_COMMIT请求时,会在等待超时之后,会继续进行事务的提交。 三阶段提交状态机 下图为三阶段提交协议中的协调者及参与者的状态机。左侧a为协调者状态机;右侧b为参与者状态机。 三阶段提交的缺陷 相对于2PC,3PC主要解决的单点故障问题,并减少阻塞,因为一旦参与者无法及时收到来自协调者的信息之后,他会默认执行commit。而不会一直持有事务资源并处于阻塞状态。但是这种机制也会导致数据一致性问题,因为,由于网络原因,协调者发送的abort响应没有及时被参与者接收到,那么参与者在等待超时之后执行了commit操作。这样就和其他接到abort命令并执行回滚的参与者之间存在数据不一致的情况。 参考引用 本文同步至: https://waylau.com/three-phase-commitment-protocol/ 分布式事务——两阶段提交: https://waylau.com/two-phase-commitment-protocol/ Distributed systems: principles and paradigms Notes on Database Operating Systems NonBlocking Commit Protocols 分布式系统常用技术及案例分析(第二版):https://github.com/waylau/distributed-systems-technologies-and-cases-analysis
在分布式系统中,为了保证数据的高可用,通常会将数据保留多个副本(replica), 这些副本会放置在不同的节点上。这些数据节点可能是物理机器,也可能是虚拟机。为了对用户提供正确的CURD等语意,我们需要保证这些放置在不同节点上的副本是一致的,这就涉及分布式事务的问题。 本文介绍分布式事务处理方案之一的两阶段提交协议。 分布式事务 分布式事务是指发生在多个数据节点之间的事务,分布式事务比单机事务要复杂的多。在分布式系统中,各个节点之间在是相互独立的,需要通过网络进行沟通和协调。由于存在事务机制,可以保证每个独立节点上的数据操作可以满足ACID。但是,相互独立的节点之间无法准确地知道其他节点的事务执行情况。所以从理论上来讲,两个节点的数据是无法达到一致的状态。如果想让分布式部署的多个节点中的数据保持一致性,那么就要保证在所有节点数据的写操作,要么全部都执行,要么全部都不执行。但是,一台机器在执行本地事务的时候无法知道其他机器中的本地事务的执行结果,所以它也就不知道本次事务到底应该commit还是rollback。所以,常规的解决办法就是引入一个"协调者"的组件来统一调度所有分布式节点的执行。 为了解决这种分布式一致性问题,前人在性能和数据一致性的反反复复权衡过程中总结了许多典型的协议和算法。其中比较著名的有二阶提交协议(Two Phase Commitment Protocol)、三阶提交协议(Three Phase Commitment Protocol)和Paxos算法。针对分布式事务,是X/Open 这个组织定义的一套分布式事务的标准X/Open DTP(X/Open Distributed Transaction Processing ReferenceModel),定义了规范和API接口,可以由各个厂商进行具体的实现。 大部分的关系型数据库通过两阶段提交(Two Phase Commit,2PC)算法来完成分布式事务,比如Oracle中通过dblink方式进行事务处理。下面重点介绍下2PC算法。 两阶段提交概述 两阶段提交协议最早是分布式事务的专家Jim Gray在1978年的一篇文章Notes on Database Operating Systems中提及。两阶段提交协议可以保证数据的强一致性,即保证了分布式事务的原子性:所有结点要么全做要么全不做。许多分布式关系型数据管理系统采用此协议来完成分布式事务。它是协调所有分布式原子事务参与者,并决定提交或取消(回滚)的分布式算法。同时也是解决一致性问题的算法。该算法能够解决很多的临时性系统故障(包括进程、网络节点、通信等故障),被广泛地使用。但是,它并不能够通过配置来解决所有的故障,在某些情况下它还需要人为的参与才能解决问题。 顾名思义,两阶段提交分为以下两个阶段: 准备阶段(Prepare Phase) 提交阶段(Commit Phase) 在两阶段提交协议中,系统一般包含两类角色: 协调者(Coordinator),通常一个系统中只有一个; 参与者(Participant),一般包含多个,在数据存储系统中可以理解为数据副本的个数。 准备阶段 在准备阶段,协调者将通知事务参与者准备提交或取消事务,写本地的redo和undo日志,但不提交,然后进入表决过程。在表决过程中,参与者将告知协调者自己的决策:同意(事务参与者本地作业执行成功)或取消(本地作业执行故障)。 协调者协议流程如下: 写本地日志“BEGIN_COMMIT”,并进入WAIT状态; 向所有参与者发送“VOTE_REQUEST”消息; 等待并接收参与者发送的对“VOTE_REQUEST”的响应。参与者响应“VOTE_ABORT”或“VOTE_COMMIT”消息给协调者。 提交阶段 在该阶段,协调者将基于第一个阶段的投票结果进行决策:提交或取消。当且仅当所有的参与者同意提交事务协调者才通知所有的参与者提交事务,否则协调者将通知所有的参与者取消事务。参与者在接收到协调者发来的消息后将执行响应的操作。 协调者协议流程如下: 若收到任何一个参与者发送的“VOTE_ABORT”消息; 写本地“GLOBAL_ABORT”日志,进入ABORT状态; 向所有的参与者发送“GLOBAL_ABORT”消息; 若收到所有参与者发送的“VOTE_COMMIT”消息; 写本地“GLOBAL_COMMIT”日志,进入COMMIT状态; 向所有的参与者发送“GLOBAL_COMMIT”消息; 等待并接收参与者发送的对“GLOBAL_ABORT”消息或“GLOBAL_COMMIT”消息的确认响应消息,一旦收到所有参与者的确认消息,写本地“END_TRANSACTION”日志流程结束。 两阶段提交状态机 下图为两阶段提交协议中的协调者及参与者的状态机。左侧a为协调者状态机;右侧b为参与者状态机。 实际案例 MySQL从5.5版本开始支持,SQL Server 2005开始支持,Oracle 7开始支持。 两阶段提交的缺陷 一般情况下,两阶段提交机制都能较好的运行,当在事务进行过程中,有参与者宕机时,重启以后,可以通过询问其他参与者或者协调者,从而知道这个事务到底提交了没有。当然,这一切的前提都是各个参与者在进行每一步操作时,都会事先写入日志。 两阶段提交不能解决的困境如下: 同步阻塞问题。执行过程中,所有参与节点都是事务阻塞型的。当参与者占有公共资源时,其他第三方节点访问公共资源不得不处于阻塞状态。 单点故障。由于协调者的重要性,一旦协调者发生故障,参与者会一直阻塞下去。尤其在第二阶段,协调者发生故障,那么所有的参与者还都处于锁定事务资源的状态中,而无法继续完成事务操作。(如果是协调者挂掉,可以重新选举一个协调者,但是无法解决因为协调者宕机导致的参与者处于阻塞状态的问题) 数据不一致。在二阶段提交的阶段二中,当协调者向参与者发送commit请求之后,发生了局部网络异常或者在发送commit请求过程中协调者发生了故障,这回导致只有一部分参与者接受到了commit请求。而在这部分参与者接到commit请求之后就会执行commit操作。但是其他部分未接到commit请求的机器则无法执行事务提交。于是整个分布式系统便出现了数据不一致性的现象。 参考引用 本文同步至: https://waylau.com/two-phase-commitment-protocol/ Distributed systems: principles and paradigms Notes on Database Operating Systems 分布式系统常用技术及案例分析:https://github.com/waylau/distributed-systems-technologies-and-cases-analysis
Angular CLI 是 Angular 客户端命令行工具,提供非常多的命令来简化 Angular 的开发。今天执行“ng serve”命令时,竟然报找不到模块"@angular-devkit/build-angular"的错误。 问题背景 执行“ng serve”命令时,竟然报找不到模块"@angular-devkit/build-angular"的错误。信息如下: >ng serve An unhandled exception occurred: Could not find module "@angular-devkit/build-angular" from "D:\\workspaceGithub\\mean-news-ui\\mean-news-ui". See "C:\Users\LYF\AppData\Local\Temp\ng-FStMRr\angular-errors.log" for further details. 解决 怀疑是 Angular CLI 与 Angular 应用版本不匹配或者是本地环境有错误引起的。 解决的方案就是卸载 Angular CLI再重新安装,错误就没有了。 1. 卸载老版本 Angular CLI 卸载老版本 Angular CLI,命令如下: >npm uninstall -g @angular/cli removed 244 packages in 20.263s 2. 验证卸载 执行 Angular CLI验证是否已经卸载成功,命令如下: >ng 'ng' 不是内部或外部命令,也不是可运行的程序 或批处理文件。 3. 清理缓存(可选) 建议清理下缓存,该步骤是可选的: >npm cache clean --force npm WARN using --force I sure hope you know what you are doing. 4. 安装新版本 Angular CLI 安装新版本 Angular CLI,命令如下: >npm install -g @angular/cli C:\Users\LYF\AppData\Roaming\npm\ng -> C:\Users\LYF\AppData\Roaming\npm\node_modules\@angular\cli\bin\ng > @angular/cli@8.3.12 postinstall C:\Users\LYF\AppData\Roaming\npm\node_modules\@angular\cli > node ./bin/postinstall/script.js + @angular/cli@8.3.12 added 244 packages from 185 contributors in 63.738s 参考引用 本文同步至: https://waylau.com/angular-could-not-find-module-build-angular/ 完整源码:https://github.com/waylau/angular-enterprise-application-development-samples Angular CLI 常用命令:https://waylau.com/angular-cli-commands/
近期 Java 界好消息频传。先是 Java 13 发布,接着 Eclipse 也发布了新版本表示支持新版本的Java 特性。本文介绍了 Java 13 的新特性并展示了相关的示例。 2019年9月17日,Java 13 正式发布。特性如下。 Java 13 新特性 此版本带来了以下几大新特性: JEP 350,Dynamic CDS Archives:扩展应用程序类-数据共享,以允许在 Java 应用程序执行结束时动态归档类。归档类将包括默认的基础层 CDS(class data-sharing)存档中不存在的所有已加载的应用程序类和库类。 JEP 351,ZGC: Uncommit Unused Memory:增强 ZGC 以将未使用的堆内存返回给操作系统。 JEP 353,Reimplement the Legacy Socket API:使用易于维护和调试的更简单、更现代的实现替换 java.net.Socket 和 java.net.ServerSocket API 使用的底层实现。 JEP 354,Switch Expressions (Preview):可在生产环境中使用的 switch 表达式,JDK 13 中将带来一个 beta 版本实现。switch 表达式扩展了 switch 语句,使其不仅可以作为语句(statement),还可以作为表达式(expression),并且两种写法都可以使用传统的 switch 语法,或者使用简化的“case L ->”模式匹配语法作用于不同范围并控制执行流。这些更改将简化日常编码工作,并为 switch 中的模式匹配(JEP 305)做好准备。 JEP 355,Text Blocks (Preview):将文本块添加到 Java 语言。文本块是一个多行字符串文字,它避免了对大多数转义序列的需要,以可预测的方式自动格式化字符串,并在需要时让开发人员控制格式。 安装 JDK 13 JDK 13下载地址为 https://www.oracle.com/technetwork/java/javase/downloads/index.html。 以Windows环境为例,可通过jdk-13_windows-x64_bin.exe或jdk-13_windows-x64_bin.zip来进行安装。 .exe文件的安装方式较为简单,按照界面提示点击“下一步”即可。 下面演示.zip安装方式。 1. 解压.zip文件到指定位置 将jdk-13_windows-x64_bin.zip文件解压到指定的目录下即可。比如,本例子放置在了D:\Program Files\jdk-13位置。 2. 设置环境变量 创建系统变量“JAVA_HOME”,其值指向了JDK的安装目录。 在用户变量“Path”中,增加“%JAVA_HOME%bin”。 注:JDK13已经无需再安装JRE,设置环境变量时也不用设置CLASSPATH了。 3. 验证安装 执行“java -version”命令进行安装的验证: $ java -version java version "13" 2019-09-17 Java(TM) SE Runtime Environment (build 13+33) Java HotSpot(TM) 64-Bit Server VM (build 13+33, mixed mode, sharing) 如果现实上述信息,则说明JDK已经安装完成。 如果显示的内容还是安装前的老JDK版本,则可按照如下步骤解决。 首先,卸载老版本的JDK 其次,在命令行输入如下指令来设置JAVA_HOM和Path: >SET JAVA_HOME=D:\Program Files\jdk-13 >SET Path=%JAVA_HOME%\bin Eclipse IDE 2019-09 在 Java 13 发布两天后的2019年9月19日,Eclipse IDE 2019-09 发布。Eclipse IDE 2019-09 声称支持Java 13。接下里将演示如何使用Eclipse IDE 2019-09编写 Java 13 的示例。 Eclipse IDE 2019-09 下载地址为 https://www.eclipse.org/downloads/packages/。本例使用的是Eclipse 4.14版本。 编写 Java 13 示例 实战1:Switch表达式的例子 下面是原有的Switch表达式的写法: switch (day) { case MONDAY: case FRIDAY: case SUNDAY: System.out.println(6); break; case TUESDAY: System.out.println(7); break; case THURSDAY: case SATURDAY: System.out.println(8); break; case WEDNESDAY: System.out.println(9); break; } 在Java 12中,Switch表达式可以改为如下写法: switch (day) { case MONDAY, FRIDAY, SUNDAY -> System.out.println(6); case TUESDAY -> System.out.println(7); case THURSDAY, SATURDAY -> System.out.println(8); case WEDNESDAY -> System.out.println(9); } 还能支持在表达式中返回值: int numLetters = switch (day) { case MONDAY, FRIDAY, SUNDAY -> 6; case TUESDAY -> 7; case THURSDAY, SATURDAY -> 8; case WEDNESDAY -> 9; }; 在Java 13中,Switch表达式可以改为如下写法: int date = switch (day) { case MONDAY, FRIDAY, SUNDAY : yield 6; case TUESDAY : yield 7; case THURSDAY, SATURDAY : yield 8; case WEDNESDAY : yield 9; default : yield 1; // default条件是必须的 }; System.out.println(date); 需要注意的是,在使用yield时,必须要有default条件。 实战2:文本块 自Java 13开始,支持文本块(Text Blocks)。 以下是Java 13之前的文本块的处理方式的示例: String html = "<html>\n" + " <body>\n" + " <p>Hello, world</p>\n" + " </body>\n" + "</html>\n"; System.out.println(html); 在上述示例中,由于文本块需要换行,所以产生了很多本文的拼接和转义。 以下是Java 13中的文本块示例: String html2 = """ <html> <body> <p>Hello, world</p> </body> </html> """; System.out.println(html2); 在上述示例中,对于文本块的处理变得简洁、自然。 以上两个示例在控制台输出内容都是一样的,效果如下: <html> <body> <p>Hello, world</p> </body> </html> 更多Java示例,可见“现代Java案例大全”。 参考引用 本文同步至: https://waylau.com/java-13-new-features-and-samples/ 完整源码:https://github.com/waylau/modern-java-demos http://openjdk.java.net/projects/jdk/13
本文介绍了如何从 Angular 中的 URL 获取查询参数。 通过注入ActivatedRoute的实例,可以订阅各种可观察对象,包括queryParams和params observable。以下是范例: import { ActivatedRoute } from '@angular/router'; // 用于获取路由参数 import { Component, OnInit } from '@angular/core'; import { DomSanitizer } from '@angular/platform-browser'; // 用于HTML过滤 import { Location } from '@angular/common'; // 用于回退浏览记录 import { NewsDetailService } from '../news-detail.service'; @Component({ selector: 'app-news-detail', templateUrl: './news-detail.component.html', styleUrls: ['./news-detail.component.css'] }) export class NewsDetailComponent implements OnInit { newsDetailData = null; newsUrl = null; constructor(private newsDetailService: NewsDetailService, private domSanitizer: DomSanitizer, private route: ActivatedRoute, private location: Location) { } ngOnInit() { this.showNewsDetailData(); } // 展示新闻详情数据 showNewsDetailData() { this.route.queryParams.subscribe(p => { this.newsUrl = p.newsUrl // 获取参数 this.newsDetailService.getNewsData(this.newsUrl).subscribe( (newsApiData) => this.newsDetailData = this.domSanitizer.bypassSecurityTrustHtml(newsApiData.toString()) //HTML过滤 ); }); } // 返回 goback() { // 浏览器回退浏览记录 this.location.back(); } } 参考引用 本文同步至: https://waylau.com/get-query-params-from-url-in-angular/ 完整源码:https://github.com/waylau/angular-enterprise-application-development-samples 《Angular企业级应用开发实战》
当往MongoDB中插入一条数据时,会自动生成ObjectId作为数据的主键。那么如何通过ObjectId来做数据的唯一查询呢? 在MongoDB中插入一条数据 在MongoDB中插入一条如下结构的数据: { _id: 5d6a32389c825e24106624e4, title: 'GitHub 上有什么好玩的项目', content: '上个月有水友私信问我,GitHub 上有没有比较好玩的项目可以推荐?我跟他说:"有,过两天我整理一下"。\n' + '\n' + '然而,一个月过去了,我把这件事情忘了精光,直至他昨天提醒我才记起2_05.png。\n', creation: 2019-08-31T08:39:20.384Z } 其中,上述_id的值“5d6a32389c825e24106624e4”,是MongoDB自动分配的。 使用 MongoDB 的 ObjectId 作为查询条件 须知,_id的值“5d6a32389c825e24106624e4”并非是字符串,而是ObjectId对象类型。因此,如下查询是行不通的: // 查询指定文档 const findNews = function (db, newsId, callback) { // 获取集合 const news = db.collection('news'); // 查询指定文档 news.findOne({_id: newsId},function (err, result) { if (err) { console.error('error end: ' + err.stack); return; } console.log("查询指定文档,响应结果是:"); console.log(result); callback(result); }); } 需将上述newsId转为 ObjectId对象类型。怎么做呢?做法参考如下: const ObjectId = require('mongodb').ObjectId; // 查询指定文档 const findNews = function (db, newsId, callback) { // 获取集合 const news = db.collection('news'); // 查询指定文档 news.findOne({_id: ObjectId(newsId)},function (err, result) { if (err) { console.error('error end: ' + err.stack); return; } console.log("查询指定文档,响应结果是:"); console.log(result); callback(result); }); } 其中,require('mongodb').ObjectId用于获取ObjectId类,并将字符串newsId转为了 ObjectId 类型。 参考引用 本文同步至: https://waylau.com/node.js-mongodb-objectid/ 完整源码:https://github.com/waylau/mean-book-samples
mysql模块(项目地址为https://github.com/mysqljs/mysql)是一个开源的、JavaScript编写的MySQL驱动,可以在Node.js应用中来操作MySQL。但在使用过程中,出现了“ER_NOT_SUPPORTED_AUTH_MODE”问题。 本文介绍了出现该问题的原因及解决方案。 报错信息 当我试图使用mysql模块来连接MySQL 8时,出现了如下错误信息: D:\workspaceGithub\nodejs-book-samples\samples\mysql-demo\index.js:17 throw error; ^ Error: ER_NOT_SUPPORTED_AUTH_MODE: Client does not support authentication protocol requested by server; consider upgrading MySQL client at Handshake.Sequence._packetToError (D:\workspaceGithub\nodejs-book-samples\samples\mysql-demo\node_modules\mysql\lib\protocol\sequences\Sequence.js:47:14) at Handshake.ErrorPacket (D:\workspaceGithub\nodejs-book-samples\samples\mysql-demo\node_modules\mysql\lib\protocol\sequences\Handshake.js:123:18) at Protocol._parsePacket (D:\workspaceGithub\nodejs-book-samples\samples\mysql-demo\node_modules\mysql\lib\protocol\Protocol.js:291:23) at Parser._parsePacket (D:\workspaceGithub\nodejs-book-samples\samples\mysql-demo\node_modules\mysql\lib\protocol\Parser.js:433:10) at Parser.write (D:\workspaceGithub\nodejs-book-samples\samples\mysql-demo\node_modules\mysql\lib\protocol\Parser.js:43:10) at Protocol.write (D:\workspaceGithub\nodejs-book-samples\samples\mysql-demo\node_modules\mysql\lib\protocol\Protocol.js:38:16) at Socket.<anonymous> (D:\workspaceGithub\nodejs-book-samples\samples\mysql-demo\node_modules\mysql\lib\Connection.js:91:28) at Socket.<anonymous> (D:\workspaceGithub\nodejs-book-samples\samples\mysql-demo\node_modules\mysql\lib\Connection.js:525:10) at Socket.emit (events.js:196:13) at addChunk (_stream_readable.js:290:12) -------------------- at Protocol._enqueue (D:\workspaceGithub\nodejs-book-samples\samples\mysql-demo\node_modules\mysql\lib\protocol\Protocol.js:144:48) at Protocol.handshake (D:\workspaceGithub\nodejs-book-samples\samples\mysql-demo\node_modules\mysql\lib\protocol\Protocol.js:51:23) at Connection.connect (D:\workspaceGithub\nodejs-book-samples\samples\mysql-demo\node_modules\mysql\lib\Connection.js:119:18) at Object.<anonymous> (D:\workspaceGithub\nodejs-book-samples\samples\mysql-demo\index.js:12:12) at Module._compile (internal/modules/cjs/loader.js:759:30) at Object.Module._extensions..js (internal/modules/cjs/loader.js:770:10) at Module.load (internal/modules/cjs/loader.js:628:32) at Function.Module._load (internal/modules/cjs/loader.js:555:12) at Function.Module.runMain (internal/modules/cjs/loader.js:826:10) at internal/main/run_main_module.js:17:11 出错原因 导致这个错误的原因是,目前,最新的mysql模块并未完全支持MySQL 8的“caching_sha2_password”加密方式,而“caching_sha2_password”在MySQL 8中是默认的加密方式。因此,下面的方式命令是默认已经使用了“caching_sha2_password”加密方式,该账号、密码无法在mysql模块中使用。 mysql> ALTER USER 'root'@'localhost' IDENTIFIED BY '123456'; Query OK, 0 rows affected (0.12 sec) 解决方法 解决方法是从新修改用户root的密码,并指定mysql模块能够支持的加密方式: mysql> ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '123456'; Query OK, 0 rows affected (0.12 sec) 上述语句,显示指定了使用“mysql_native_password”的加密方式。这种方式是在mysql模块能够支持。 再此运行应用,可以看到如下的控制台输出信息: $ node index.js The result is: RowDataPacket { user_id: 1, username: '老卫' } 其中,“RowDataPacket { user_id: 1, username: '老卫' }”就是数据库查询的结果。 源码 本节例子可以在https://github.com/waylau/nodejs-book-samples的“mysql-demo”应用中找到。 参考引用 本文同步至: https://waylau.com/node.js-mysql-client-does-not-support-authentication-protocol/ 有关MySQL 8的“caching_sha2_password”加密方式,可见https://dev.mysql.com/doc/refman/8.0/en/upgrading-from-previous-series.html#upgrade-caching-sha2-password
对于聊天室,大家应该都不陌生,笔者也写过很多关于聊天室的例子。 本节,我们将演示如何通过Node.js来实现一个WebSocket聊天服务器的例子。 使用ws创建WebSokcet服务器 Node.js原生API并未提供WebSocket的支持,因此,需要安装第三方包才能使用WebSocket功能。对于WebSocket的支持,在开源社区有非常多的选择,本例子采用的是“ws”框架(项目主页为https://github.com/websockets/ws)。 “ws”顾名思义是一个用于支持WebSocket客户端和服务器的框架。它易于使用,功能强大,且不依赖于其他环境。 想其他Node.js应用一样,使用ws的首选方式是使用npm来管理。以下命令行用于安装ws在应用里面: npm install ws 具备了ws包之后,就可以创建WebSocket服务器了。以下是创建服务器的j简单示例: const WebSocket = require('ws'); const server = new WebSocket.Server({ port: 8080 }); 上述例子服务器启动在8080端口。 聊天服务器的需求 聊天服务器的业务需求比较简单,是一个群聊聊天室。换言之,所有人发送的消息大家都可以见到。 当有新用户连接到服务器时,会以该用户的“IP+端口”作为用户的名称。 服务器的实现 根据前面知识的学习,实现一个聊天服务器比较简单,完整代码如下: const WebSocket = require('ws'); const server = new WebSocket.Server({ port: 8080 }); server.on('open', function open() { console.log('connected'); }); server.on('close', function close() { console.log('disconnected'); }); server.on('connection', function connection(ws, req) { const ip = req.connection.remoteAddress; const port = req.connection.remotePort; const clientName = ip + port; console.log('%s is connected', clientName) // 发送欢迎信息给客户端 ws.send("Welcome " + clientName); ws.on('message', function incoming(message) { console.log('received: %s from %s', message, clientName); // 广播消息给所有客户端 server.clients.forEach(function each(client) { if (client.readyState === WebSocket.OPEN) { client.send( clientName + " -> " + message); } }); }); }); 当客户端给服务器发送消息时,服务器会将该客户端的消息转发给所有客户端。 客户端的实现 客户端是通HTML+JavaScript的方式实现的。由于浏览器原生提供了WebSocket的API,所以并不需要ws框架的支持。 客户端client.html文件代码如下: <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>WebSocket Chat</title> </head> <body> <script type="text/javascript"> var socket; if (!window.WebSocket) { window.WebSocket = window.MozWebSocket; } if (window.WebSocket) { socket = new WebSocket("ws://localhost:8080/ws"); socket.onmessage = function (event) { var ta = document.getElementById('responseText'); ta.value = ta.value + '\n' + event.data }; socket.onopen = function (event) { var ta = document.getElementById('responseText'); ta.value = "连接开启!"; }; socket.onclose = function (event) { var ta = document.getElementById('responseText'); ta.value = ta.value + "连接被关闭"; }; } else { alert("你的浏览器不支持 WebSocket!"); } function send(message) { if (!window.WebSocket) { return; } if (socket.readyState == WebSocket.OPEN) { socket.send(message); } else { alert("连接没有开启."); } } </script> <form onsubmit="return false;"> <h3>WebSocket 聊天室:</h3> <textarea id="responseText" style="width: 500px; height: 300px;"></textarea> <br> <input type="text" name="message" style="width: 300px" value="Welcome to waylau.com"> <input type="button" value="发送消息" onclick="send(this.form.message.value)"> <input type="button" onclick="javascript:document.getElementById('responseText').value=''" value="清空聊天记录"> </form> <br> <br> <a href="https://waylau.com/">更多例子请访问 waylau.com</a> </body> </html> 运行应用 首先启动服务器。执行下面的命令: node index.js 接着用浏览器直接打开client.html文件,可以看到如下的聊天界面。 打开多个聊天窗口,就能模拟多个用户之间的群聊了。 源码 本节例子可以在https://github.com/waylau/nodejs-book-samples的“ws-demo”应用中找到。 本节例子可以在“ws-demo”应用中找到。 参考引用 本文同步至: https://waylau.com/node.js-websocket-chat/ MINA 实现聊天功能: https://waylau.com/mina-chat/ Netty 实现 WebSocket 聊天功能: https://waylau.com/netty-websocket-chat/ Netty 实现聊天功能: https://waylau.com/netty-chat/ WebSocket 和 Golang 实现聊天功能: https://waylau.com/go-websocket-chat/
12-Factor(twelve-factor),也称为“十二要素”,是一套流行的应用程序开发原则。Cloud Native架构中使用12-Factor作为设计准则。 12-Factor 的目标在于: 使用标准化流程自动配置,从而使新的开发者花费最少的学习成本加入项目中。 和底层操作系统之间尽可能的划清界限,在各个系统中提供最大的可移植性。 适合部署在现代的云计算平台,从而在服务器和系统管理方面节省资源。 将开发环境和生产环境的差异降至最低,并使用持续交付实施敏捷开发。 可以在工具、架构和开发流程不发生明显变化的前提下实现扩展。 12-Factor 可以适用于任意语言和后端服务(数据库、消息队列、缓存等)开发的应用程序,自然也适用于 Cloud Native。在构建 Cloud Native 应用时,也需要考虑这十二个方面的内容。 1 基准代码 代码是程序的根本,有什么样的代码最终会表现为怎么样的程序软件。从源码到产品发布中间会经历多个环节,比如开发、编译、测试、构建、部署等,这些环节可能都有自己的不同的部署环境,而不同的环境相应的责任人关注于产品的不同阶段。比如,测试人员主要关注于测试的结果,而业务人员可能关注于生产环境的最终的部署结果。但不管是哪个环节,部署到怎么的环境中,他们所依赖的代码是一致的,即所谓的“一份基准代码(Codebase),多份部署(Deploy)”。 现代的代码管理,往往需要进行版本的管理。即便是个人的工作,采用版本管理工具进行管理,对于方便查找特定版本的内容,或者是回溯历史的修改内容都是极其必要。版本控制系统就是帮助人们协调工作的工具,它能够帮助我们和其他小组成员监测同一组文件,比如说软件源代码,升级过程中所做的变更,也就是说,它可以帮助我们轻松地将工作进行融合。 版本控制工具发展到现在已经有几十年了,简单地可以将其分为四代: 文件式版本控制系统,比如 SCCS、RCS; 树状版本控制系统—服务器模式,比如 CVS; 树状版本控制系统—双服务器模式,比如 Subversion; 树状版本控制系统—分布式模式,比如 Bazaar、Mercurial、Git。 目前,在企业中广泛采用服务器模式的版本控制系统,但越来越多的企业开始倾向于采用分布式模式版本控制系统。 读者如果对版本控制系统感兴趣,可以参阅笔者所著的《分布式系统常用技术及案例分析》中的“第7章分布式版本控制系统”内容。本书“10.3 代码管理”章节部分,还会继续深入探讨 Git 的使用。 2 依赖 应该明确声明应用程序依赖关系(Dpendency),这样,所有的依赖关系都可以从工件的存储库中获得,并且可以使用依赖管理器(例如 Apache Maven、Gradle)进行下载。 显式声明依赖的优点之一是为新进开发者简化了环境配置流程。新进开发者可以检出应用程序的基准代码,安装编程语言环境和它对应的依赖管理工具,只需通过一个构建命令来安装所有的依赖项,即可开始工作。 比如,项目组统一采用 Gradle 来进行依赖管理。那么可以使用 Gradle Wrapper。Gradle Wrapper 免去了用户在使用 Gradle 进行项目构建时需要安装 Gradle 的繁琐步骤。每个 Gradle Wrapper 都绑定到一个特定版本的 Gradle,所以当你第一次在给定 Gradle 版本下运行上面的命令之一时,它将下载相应的 Gradle 发布包,并使用它来执行构建。默认,Gradle Wrapper 的发布包是指向的官网的 Web 服务地址,相关配置记录在了 gradle-wrapper.properties 文件中。我们查看下 Sring Boot 提供的这个 Gradle Wrapper 的配置,参数“distributionUrl”就是用于指定发布包的位置。 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-4.5.1-bin.zip 而这个 gradle-wrapper.properties 文件是作为依赖项,而纳入代码存储库中的。 3 配置 相同的应用,在不同的部署环境(如预发布、生产环境、开发环境等等)下,可能有不同的配置内容。这其中包括: 数据库、Redis 以及其他后端服务的配置; 第三方服务的证书; 每份部署特有的配置,如域名等。 这些配置项不可能硬编码在代码中,因为我们必须要保证同一份基准代码(Codebase)能够多份部署。一种解决方法是使用配置文件,但不把它们纳入版本控制系统,就像 Rails 的 config/database.yml。这相对于在代码中硬编码常量已经是长足进步,但仍然有缺点: 不小心将配置文件签入了代码库; 配置文件的可能会分散在不同的目录,并有着不同的格式,不方便统一管理; 这些格式通常是语言或框架特定的,不具备通用性。 所以,推荐的做法是将应用的配置存储于环境变量中。好处在于: 环境变量可以非常方便地在不同的部署间做修改,却不动一行代码; 与配置文件不同,不小心把它们签入代码库的概率微乎其微; 与一些传统的解决配置问题的机制(比如 Java 的属性配置文件)相比,环境变量与语言和系统无关。 本书介绍了另外一种解决方案——集中化配置中心。通过配置中心来集中化管理各个环境的配置变量。配置中心的实现也是于具体语言和系统无关的。欲了解有关配置中心的内容,可以参阅本书“10.5 配置管理”章节的内容。 4 后端服务 后端服务(Backing Services)是指程序运行所需要的通过网络调用的各种服务,如数据库(MySQL,CouchDB),消息/队列系统(RabbitMQ,Beanstalkd),SMTP 邮件发送服务(Postfix),以及缓存系统(Memcached,Redis)。 这些后端服务,通常由部署应用程序的系统管理员一起管理。除了本地服务之外,应用程序有可能使用了第三方发布和管理的服务。示例包括 SMTP(例如 Postmark),数据收集服务(例如 New Relic 或 Loggly),数据存储服务(如 Amazon S3),以及使用 API 访问的服务(例如 Twitter、Google Maps 等等)。 12-Factor 应用不会区别对待本地或第三方服务。对应用程序而言,本地或第三方服务都是附加资源,都可以通过一个 URI 或是其他存储在配置中的服务定位或服务证书来获取数据。12-Factor 应用的任意部署,都应该可以在不进行任何代码改动的情况下,将本地 MySQL 数据库换成第三方服务(例如 Amazon RDS)。类似的,本地 SMTP 服务应该也可以和第三方 SMTP 服务(例如 Postmark )互换。比如,在上述两个例子中,仅需修改配置中的资源地址。 每个不同的后端服务都是一份资源 。例如,一个 MySQL 数据库是一个资源,两个 MySQL 数据库(用来数据分区)就被当作是两个不同的资源。12-Factor 应用将这些数据库都视作附加资源,这些资源和它们附属的部署保持松耦合。 使用后端服务的好处在于,部署可以按需加载或卸载资源。例如,如果应用的数据库服务由于硬件问题出现异常,管理员可以从最近的备份中恢复一个数据库,卸载当前的数据库,然后加载新的数据库,整个过程都不需要修改代码。 5 构建、发布、运行 基准代码进行部署需要以下三个阶段: 构建阶段:是指将代码仓库转化为可执行包的过程。构建时会使用指定版本的代码,获取和打包依赖项,编译成二进制文件和资源文件。 发布阶段:会将构建的结果和当前部署所需配置相结合,并能够立刻在运行环境中投入使用。 运行阶段:是指针对选定的发布版本,在执行环境中启动一系列应用程序进程。 应用严格区分构建、发布、运行这三个步骤。举例来说,直接修改处于运行状态的代码是非常不可取的做法,因为这些修改很难再同步回构建步骤。 部署工具通常都提供了发布管理工具,在必要的时候还是可以退回至较旧的发布版本。 每一个发布版本必须对应一个唯一的发布 ID,例如可以使用发布时的时间戳(2011-04-06-20:32:17),亦或是一个增长的数字(v100)。发布的版本就像一本只能追加的账本,一旦发布就不可修改,任何的变动都应该产生一个新的发布版本。 新的代码在部署之前,需要开发人员触发构建操作。但是,运行阶段不一定需要人为触发,而是可以自动进行。如服务器重启,或是进程管理器重启了一个崩溃的进程。因此,运行阶段应该保持尽可能少的模块,这样假设半夜发生系统故障而开发人员又捉襟见肘也不会引起太大问题。构建阶段是可以相对复杂一些的,因为错误信息能够立刻展示在开发人员面前,从而得到妥善处理。 6 进程 12-Factor 应用推荐以一个或多个无状态进程运行应用。这里的“无状态”是与 REST 中的无状态是一个意思,即进程的执行不依赖于上一个进程的执行。 举例来说,内存区域或磁盘空间可以作为进程在做某种事务型操作时的缓存,例如下载一个很大的文件,对其操作并将结果写入数据库的过程。12-Factor 应用根本不用考虑这些缓存的内容是不是可以保留给之后的请求来使用,这是因为应用启动了多种类型的进程,将来的请求多半会由其他进程来服务。即使在只有一个进程的情形下,先前保存的数据(内存或文件系统中)也会因为重启(如代码部署、配置更改、或运行环境将进程调度至另一个物理区域执行)而丢失。 一些互联网应用依赖于“粘性 session”, 这是指将用户 session 中的数据缓存至某进程的内存中,并将同一用户的后续请求路由到同一个进程。粘性 session 是 12-Factor 极力反对的。Session 中的数据应该保存在诸如 Memcached 或 Redis 这样的带有过期时间的缓存中。 相比于有状态的应用而言,无状态具有更好的可扩展性。 7 端口绑定 传统的互联网应用有时会运行于服务器的容器之中。例如 PHP 经常作为 Apache HTTPD 的一个模块来运行,而 Java 应用往往会运行于 Tomcat 中。 12-Factor 应用完全具备自我加载的能力,而不依赖于任何网络服务器就可以创建一个面向网络的服务。互联网应用通过端口绑定(Port binding)来提供服务,并监听发送至该端口的请求。 举例来说,Java 程序完全能够内嵌一个 Tomcat 在程序中,从而自己就能启动并提供服务,省去了将 Java 应用部署到 Tomcat 中的繁琐过程。在这方面,Spring Boot 框架的布道者 Josh Long 有句名言“Make JAR not WAR”,即 Java 应用程序应该被打包为可以独立运行的 JAR 文件,而不是传统的 WAR 包。 以 Spring Boot 为例,构建一个具有内嵌容器的 Java 应用是非常简单的,只需要引入以下依赖: // 依赖关系 dependencies { // 该依赖用于编译阶段 compile('org.springframework.boot:spring-boot-starter-web') } 这样,该 Spring Boot 应用就包含了内嵌 Tomcat 容器。 如果想使用其他容器,比如 Jetty、Undertow 等,只需要在依赖中加入相应 Servlet 容器的 Starter 就能实现默认容器的替换,比如: spring-boot-starter-jetty:使用 Jetty 作为内嵌容器,可以替换 spring-boot-starter-tomcat; spring-boot-starter-undertow:使用 Undertow 作为内嵌容器,可以替换 spring-boot-starter-tomcat。 可以使用 Spring Environment 属性配置常见的 Servlet 容器的相关设置。通常您将在 application.properties 文件中来定义属性。 常见的 Servlet 容器设置包括: 网络设置:监听 HTTP 请求的端口(server.port)、绑定到 server.address 的接口地址等; 会话设置:会话是否持久(server.session.persistence)、会话超时(server.session.timeout)、会话数据的位置(server.session.store-dir)和会话 cookie 配置(server.session.cookie.*); 错误管理:错误页面的位置(server.error.path)等; SSL; HTTP 压缩。 Spring Boot 尽可能地尝试公开这些常见公用设置,但也会有一些特殊的配置。对于这些例外的情况,Spring Boot 提供了专用命名空间来对应特定于服务器的配置(比如 server.tomcat 和 server.undertow)。 8 并发 在 12-factor 应用中,进程是一等公民。由于进程之间不会共享状态,这意味着应用可以通过进程的扩展来实现并发。 类似于 unix 守护进程模型,开发人员可以运用这个模型去设计应用架构,将不同的工作分配给不同的进程。例如,HTTP 请求可以交给 web 进程来处理,而常驻的后台工作则交由 worker 进程负责。 在 Java 语言中,往往通过多线程的方式来实现程序的并发。线程允许在同一个进程中同时存在多个线程控制流。线程会共享进程范围内的资源,例如内存句柄和文件句柄,但每个线程都有各自的程序计数器、栈以及局部变量。线程还提供了一种直观的分解模式来充分利用操作系统中的硬件并行性,而在同一个程序中的多个线程也可以被同时调度到多个CPU上运行。 毫无疑问,多线程编程使得程序任务并发成为了可能。而并发控制主要是为了解决多个线程之间资源争夺等问题。并发一般发生在数据聚合的地方,只要有聚合,就有争夺发生,传统解决争夺的方式采取线程锁机制,这是强行对CPU管理线程进行人为干预,线程唤醒成本高,新的无锁并发策略来源于异步编程、非阻塞I/O等编程模型。 并发的使用并非没有风险。多线程并发会带来如下的问题: 安全性问题。在没有充足同步的情况下,多个线程中的操作执行顺序是不可预测的,甚至会产生奇怪的结果。线程间的通信主要是通过共享访问字段及其字段所引用的对象来实现的。这种形式的通信是非常有效的,但可能导致两种错误:线程干扰(thread interference)和内存一致性错误(memory consistency errors)。 活跃度问题。一个并行应用程序的及时执行能力被称为它的活跃度(liveness)。安全性的含义是“永远不发生糟糕的事情”,而活跃度则关注于另外一个目标,即“某件正确的事情最终会发生”。当某个操作无法继续执行下去,就会发生活跃度问题。在串行程序中,活跃度问题形式之一就是无意中造成的无限循环(死循环)。而在多线程程序中,常见的活跃度问题主要有死锁、饥饿以及活锁。 性能问题。在设计良好的并发应用程序中,线程能提升程序的性能,但无论如何,线程总是带来某种程度的运行时开销。而这种开销主要是在线程调度器临时关起活跃线程并转而运行另外一个线程的上下文切换操作(Context Switch)上,因为执行上下文切换,需要保存和恢复执行上下文,丢失局部性,并且CPU时间将更多地花在线程调度而不线程运行上。当线程共享数据时,必须使用同步机制,而这些机制往往会抑制某些编译器优化,使内存缓存区中的数据无效,以及增加贡献内存总线的同步流量。所以这些因素都会带来额外的性能开销。 9 易处理 12-Factor 应用的进程是易处理(Disposable)的,意味着它们可以瞬间启动或停止。比如,Spring Boot 应用,它可以无需依赖容器,而采用内嵌容器的方式来实现自启动。这有利于迅速部署变化的代码或配置,保障系统的可用性,并在系统负荷到来前,快速实现扩展。 进程应当追求最小启动时间。 理想状态下,进程从敲下命令到真正启动并等待请求的时间应该只需很短的时间。更少的启动时间提供了更敏捷的发布以及扩展过程,此外还增加了健壮性,因为进程管理器可以在授权情形下容易的将进程搬到新的物理机器上。 进程一旦接收终止信号(SIGTERM)就会优雅的终止。就网络进程而言,优雅终止是指停止监听服务的端口,即拒绝所有新的请求,并继续执行当前已接收的请求,然后退出。 对于 worker 进程来说,优雅终止是指将当前任务退回队列。例如,RabbitMQ 中,worker 可以发送一个 NACK 信号。Beanstalkd 中,任务终止并退回队列会在 worker 断开时自动触发。有锁机制的系统诸如 Delayed Job 则需要确定释放了系统资源。 10 开发环境与线上环境等价 我们期望一份基准代码可以部署到多个环境,但如果环境不一致,最终也可能导致运行程序的结果不一致。 比如,在开发环境,我们是采用了 MySQL 作为测试数据库,而在线上生产环境,则是采用了 Oracle。虽然,MySQL 和 Oracle 都遵循相同的 SQL 标准,但两者在很多语法上还是存在细微的差异。这些差异非常有可能导致两者的执行结果不一致,甚至某些 SQL 语句在开发环境能够正常执行,而在线上环境根本无法执行。这都给调试增加了复杂性,同时,也无法保障最终的测试效果。 所以,一个好的指导意见是,不同的环境尽量保持一样。开发环境、测试环境与线上环境设置成一样,更早发现测试问题,而不至于在生产环境才暴露出问题。 11 日志 在应用程序中打日志是一个好习惯。日志使得应用程序运行的动作变得透明。日志是在系统出现故障时,排查问题的有力帮手。 日志应该是事件流的汇总,将所有运行中进程和后端服务的输出流按照时间顺序收集起来。尽管在回溯问题时可能需要看很多行,日志最原始的格式确实是一个事件一行。日志没有确定开始和结束,但随着应用在运行会持续的增加。对于传统的 Java EE 应用程序而言,有许多框架和库可用于日志记录。Java Logging (JUL) 是 Java 自身所提供的现成选项。除此之外 Log4j、Logback 和 SLF4J 是其他一些流行的日志框架。 对于传统的单块架构而言,日志管理本身并不存在难点,毕竟所有的日志文件,都存储在应用所部属的主机上,获取日志文件或者搜索日志内容都比较简单。但在 Cloud Native 应用中,情况则有非常大的不同。分布式系统,特别是微服务架构所带来的部署应用方式的重大转变,都使得微服务的日志管理面临很多新的挑战。一方面随着微服务实例的数量的增长,伴随而来的就是日志文件的递增。另一方面,日志被散落在各自的实例所部署的主机上,不方面整合和回溯。 在这种情况下,将日志进行集中化的管理变得意义重大。本书的“10.4 日志管理”章节内容,会对 Cloud Native 的日志集中化管理进行详细的探讨。 12 管理进程 开发人员经常希望执行一些管理或维护应用的一次性任务,例如: 运行数据移植(Django 中的 manage.py migrate, Rails 中的 rake db:migrate)。 运行一个控制台(也被称为 REPL shell),来执行一些代码或是针对线上数据库做一些检查。大多数语言都通过解释器提供了一个 REPL 工具(python 或 perl),或是其他命令(Ruby 使用 irb, Rails 使用 rails console)。 运行一些提交到代码仓库的一次性脚本。 一次性管理进程应该和正常的常驻进程使用同样的环境。这些管理进程和任何其他的进程一样使用相同的代码和配置,基于某个发布版本运行。后台管理代码应该随其他应用程序代码一起发布,从而避免同步问题。 所有进程类型应该使用同样的依赖隔离技术。例如,如果 Rub y的 web 进程使用了命令 bundle exec thin start,那么数据库移植应使用 bundle exec rake db:migrate。同样的,如果一个 Python 程序使用了 Virtualenv,则需要在运行 Tornado Web 服务器和任何 manage.py 管理进程时引入 bin/python。
当今软件行业正发生着巨变。自上世纪50年代计算机诞生以来,软件从最初的手工作坊式的交付方式,逐渐演变成为了职业化开发、团队化开发,进而定制了软件件行业的相关规范,形成了软件产业。 今天,无论是大型企业还是个人开发者,都或多或少采用了云的方式来开发、部署应用。不管是私有云,还是公有云,都终将给整个软件产业带来的革命。个人计算机或者以手机为代表的智能设备已经走进寻常百姓家了。每个人几乎都拥有手机,手机不仅仅是通信工具,还能发语音、看视频、玩游戏,让人与人之间的联系变得更加紧密。智能手环随时监控你的身体状况,并根据你每天的运动量、身体指标来给你提供合理的饮食运动建议。出门逛街甚至不需要带钱包了,吃饭购物搭车时使用手机就可以支付费用,多么方便快捷。智能家居系统更是你生活上的“管家”,什么时候该睡觉了,智能家居系统就自动拉上窗帘,关灯;早上起床了,智能家居系统会自动拉开窗帘,并播放动人的音乐,让你可以愉快地享受新的一天的来临;你再也不用担心家里的安全情况,智能家居系统会帮你监控一切,有异常情况时会及时发送通知到你的手机,让你第一时间掌握家里的状况。未来,每个人都能够拥有《Iron Man》(钢铁侠)中所描述的智能管家 Jarvis。而这一切,都离不开背后那个神秘的巨人——分布式系统。正是那些看不见的分布式系统,每天处理着数以亿计的计算,提供可靠而稳定的服务。这些系统往往是以 Cloud Native 方式来部署、运维的。 软件需求的发展 早期的软件,大多数是由使用该软件的个人或机构研制的,所以软件往往带有非常强烈的个人色彩。早期的软件开发也没有什么系统的方法论可以遵循,完全是“个人英雄主义”,也不存在所谓的软件设计,纯粹就是某个人的头脑中思想的表达。而且,当时的软件往往是围绕硬件的需求来定制化开发的,有什么样的硬件就有什么样的软件。所以,软件缺乏通用性。同时,由于软件开发过程不需要与他人协作,所以,软件除了源代码外,往往没有软件设计、使用说明书等文档。这样,就造成了软件行业缺乏经验的传承。 从60年代中期到70年代中期是计算机系统发展的第二个时期,在这一时期软件开始被当作一种产品被广泛使用。所谓产品,就是可以提供给不同的人使用,从而提高了软件的重用率,降低了软件开发的成本。比如,以前,一套软件,只能专门提供给某个人使用。现在,同一套软件可以批量的卖给不同的人,显然,分摊到相同软件上的开发成本而言,卖的越多,成本自然就越低。这个时期,出现了类似“软件作坊”的专职替别人的开发软件的团体。虽然是团体协作,但软件开发的方法基本上仍然沿用早期的个体化软件开发方式,这样导致的问题是,软件的数量急剧膨胀,软件需求日趋复杂,软件的维护难度也就越来越大,开发成本变得越来越高了,从而导致软件项目频频遭遇失败。这就演变成了“软件危机”。 “软件危机”迫使人们开始思考软件的开发方式,使得人们开始对软件及其特性进行了更加深入的研究,人们对待软件的观念也在发生悄然的改变。在早期,由于计算机的数量很少,只有少数军方或者科研机构才有机会接触到计算机,这就让大多数人认为,软件开发人员都是稀少且优秀的(一开始确实也是如此,毕竟计算机最初制作者都是数学界的天才)。由于,软件开发的技能,只能被少数人所掌握,所以大多数人对于“什么是好的软件”缺乏共识。实际上,早期那些被认为是优秀的程序常常很难被别人看懂,里面充斥着各种程序技巧。加之,当时的硬件资源比较紧缺,迫使开发人员在编程时,往往需要考虑更少的占用机子资源,从而会采用不易阅读的“精简”方式来开发,这更加加重了软件的个性化。而现在人们普遍认为,优秀的程序除了功能正确,性能优良之外,还应该更加让人容易看懂、容易使用、容易修改和扩充。这就是软件可维护性的要求。 1968年 NATO 会议上首次提出“软件危机”(Software Crisis)这个名词,同时,提出了期望通过“软件工程(Sotfwrae Engineeirng)”来解决“软件危机”。“软件工程”的目的,就是要把软件开发从“艺术”和“个体行为”向“工程”和“群体协同工作”进行转化,从而解决“软件危机”包含两方面问题: 如何开发软件,以满足不断增长,日趋复杂的需求; 如何维护数量不断膨胀的软件产品。 事实证明,在软件的可行性分析方面,事先对软件的进行可行性分析,可以有效的规避软件失败的风险,提高软件的开发的成功率。 在需求方面,软件行业的规范是,需要制定相应的软件规格说明书、软件需求说明书,从而让开发工作有了依据,划清了开发边界,并在一定程度上减少了“需求蔓延”的情况的发生。 在架构设计方面,需制定软件架构说明书,划分了系统之间的界限,约定了系统间的通信接口,并将系统分为多个模块。这样,更容易将任务分解,从而降低系统的复杂性。 今天,制定软件需求的方式越来越多样化了。客户与系统分析师也许只是经过简单的口头讨论,制定了粗略的协议,就安排开发工程师进行原型设计了。开发工程师开发一个微服务,并部署到云容器中,从而可以实现软件的交付。甚至,可以不用编写任何写后台代码,直接使用云服务供应商所提供的 API,使应用快速推向市场。客户在使用完这个应用时,马上就能将自己体验反馈到开发团队,使开发团队能够快速的响应客户的需求变化,并促使软件进行升级。 通过敏捷的方式,最终软件形成了“开发——测试——部署——反馈”的良性循环,软件产品也得到了进化。而这整个过程,都比传统的需求获取的方式将更加的迅捷。 开发方式的巨变 早些年,瀑布模型还是标准的软件开发模型。瀑布模型将软件生命周期划分为制定计划、需求分析、软件设计、程序编写、软件测试和运行维护等六个基本活动,并且规定了它们自上而下、相互衔接的固定次序,如同瀑布流水,逐级下落。在瀑布模型中,软件开发的各项活动严格按照线性方式进行,当前活动接受上一项活动的工作结果,实施完成所需的工作内容。当前活动的工作结果需要进行验证,如验证通过,则该结果作为下一项活动的输入,继续进行下一项活动,否则返回修改。 瀑布模型优点是严格遵循预先计划的步骤顺序进行,一切按部就班,整个过程比较严谨。同时,瀑布模型强调文档的作用,并要求每个阶段都要仔细验证文档的内容。但是,这种模型的线性过程太理想化,主要存在以下几个方面的问题在: 各个阶段的划分完全固定,阶段之间产生大量的文档,极大地增加了工作量; 由于开发模型是线性的,用户只有等到整个过程的末期才能见到开发成果,从而增加了开发的风险; 早期的错误可能要等到开发后期的测试阶段才能发现,进而带来严重的后果; 各个软件生命周期衔接花费时间较长,团队人员交流成本大。 瀑布式方法在需求不明并且在项目进行过程中可能变化的情况下基本是不可行的,所以瀑布式方法非常适合需求明确的软件开发。但在如今,时间就是金钱,如何快速抢占市场,是每个互联网企业需要考虑的第一要素。所以,快速迭代、频繁发布的原型开发、敏捷开发方式,被越来越多的互联网企业所采用。甚至,很多传统企业,也在逐步向敏捷的“短平快”的开发方式靠拢。毕竟,谁愿意等待呢? 客户将需求告诉了你,当然是越快希望得到反馈越好,那么,最快的方式莫过于在原有系统的基础上,搭建一个原型提供给客户作为参考。客户拿到原型之后,肯定会反馈他的意见,是好或者坏的方面都会有。这样,开发人员就能根据客户的反馈,来对原型进行快速更改,快速发布新的版本,从而实现了良好的反馈闭环。 今天,Cloud Native 的开发方式正在流行。Cloud Native 是以云架构为优先的应用开发模式。Cloud Native 利于最大化整合现有的云计算所提供的资源,同时也最大化节约了项目启动的成本。 云是大势所趋 目前,越来越多的企业已经在大规模开始拥抱云,在云环境开发应用、部署应用、发布应用。未来,越来越多的开发者也将采用 Cloud Native 来开发应用。 那么,为什么我们需要使用 Cloud Native? 云计算的第一个浪潮是关于成本节约和业务敏捷性,尤其是云计算的基础设施更加廉价。随着云计算的不断发展,企业开始采用基础架构即服务(IaaS)和平台即服务(PaaS)服务,并利用它们构建利用云的弹性和可伸缩性的应用程序,同时也能够满足云环境下的容错性。 很多企业倾向于使用微服务架构来开发应用。微服务开发快速,职责单一,能够更快速的被客户所采纳。同时,这些应用能够通过快速迭代的方式,得到进化,赢得客户的认可。Cloud Native 可以打通微服务开发、测试、部署、发布的整个流程环节。 云供应商为迎合市场,提供了满足各种场景方案的 API,例如用于定位的 Google Maps,用于社交协作的认证平台等。将所有这些 API 与企业业务的特性和功能混合在一起,可以让他们为客户构建独特的方案。所有这些整合都在 API 层面进行。这意味着,不管是移动应用还是传统的桌面应用都能无缝集成。所以,采用 Cloud Native 所开发的应用都且具备极强的可扩展性。 软件不可能不出故障。传统的企业级开发方式,需要有专职人员来对企业应用进行监控与维护。而在 Cloud Native 架构下,底层的服务或者是 API 都由将部署到云中,等价于将繁重的运维工作转移给了云平台供应商。这意味着客户应用将得到更加专业的看护,同时,也节省了运维成本。 那么如何来实现 Cloud Native 呢?其实这是一个非常大的话题,比如,作为开发者,你需要了解目前市面上流行的云供应商,了解微服务、SOA,了解 HTTP 和 REST,了解领域驱动设计(DDD),了解CICD和TDD,了解两个披萨,了解分布式的常用架构和模式等等。这里每一样都是一个庞大的课题,还好目前市面上已经有了一些资料可供学习,比如《Cloud Native 分布式架构原理与实践》,可以非常全面的指导开发者轻松入门 Cloud Native。 参考引用 原文同步至:https://waylau.com/the-way-to-cloud-native/ Cloud Native 案例大全 简述 Microservices(微服务) Spring Boot 企业级应用开发实战 Spring Cloud 微服务架构开发实战 Cloud Native 分布式架构原理与实践 分布式系统常用技术及案例分析
目前,越来越多的企业已经在大规模开始拥抱云,在云环境开发应用、部署应用、发布应用。Cloud Native(云原生)是以云架构为优先的应用开发模式。那么,为什么说 Cloud Native 是未来开发应用的趋势呢?本文一一解答。 什么是 Cloud Native Cloud Native (国内译为“云原生”),最早是 Matt Stine 提出的一个概念。与微服务一样,Cloud Native 并不是一种具体的技术,而是一类思想的集合,包括DevOps、持续交付(Continuous Delivery)、微服务(MicroServices)、敏捷基础设施(Agile Infrastructure)、康威定律(Conways Law)等,以及根据商业能力对公司进行重组。Cloud Native 既包含技术(微服务,敏捷基础设施),也包含管理(DevOps,持续交付,康威定律,重组等)。所以,Cloud Native 也可以说是一系列Cloud技术、企业管理方法的集合。 有关Cloud Native的概述,可见“简述什么是 Cloud Native(云原生)”一文的论述。 为什么说 Cloud Native 是大势所趋 目前,越来越多的企业已经开始拥抱云,在云环境下开发应用、部署应用和发布应用。未来,越来越多的开发者也将采用 Cloud Native 来开发应用。 那么,为什么说 Cloud Native 是大势所趋 1. 云计算带来的是成本的节约和业务的敏捷性 特别是使用云计算所提供的基础设施,费用会更加低廉。随着云计算的不断发展,企业越来越倾向于使用 IaaS(基础设施即服务)和 PaaS(平 台即服务)来构建应用程序。这种应用可以利用云计算的弹性和可伸缩性,同时还能满足云环境下的容错性。 2. 很多企业倾向于使用微服务架构来开发应用 微服务开发快速、职责单一,能够更快速地被客户所采纳。同时,这些应用能够通过快速迭代的方式得到进化,赢得客户的认可。Cloud Native 可以打通微服务开发、测试、部署、发布的整个流程环节。 3. 云供应商为迎合市场,提供了满足各种场景方案的 API 例如,用于定位的 Google Maps,用于社交协作的认证平台等。将这些 API 与企业业务的特性和功能结合在一起,可以让它们为客户构建独特的方案。所有整合都在 API 层面进行。这意味着,无论是移动应用还是传统的桌面应用都能无缝集成。所以,采用 Cloud Native 所开发的应用都具备极强的可扩展性。 4. 软件不可能不出故障 传统的企业级开发方式需要有专职人员来对企业应用进行监控与维护。而在 Cloud Native 架构下,底层的服务或 API 都将部署到云中,相当于将繁重的运维工作转 移给了云平台供应商。这意味着客户应用将得到更加专业的看护,同时也节省了运维成本。 如何实现 Cloud Native 那么如何来实现 Cloud Native 呢?其实这是一个非常大的话题,比如,作为开发者,你需要了解目前市面上流行的云供应商,了解微服务、SOA,了解 HTTP 和 REST,了解领域驱动设计(DDD),了解CICD和TDD,了解两个披萨,了解分布式的常用架构和模式等等。这里每一样都是一个庞大的课题,还好目前市面上已经有了一些资料可供学习,比如《Cloud Native 分布式架构原理与实践》,可以非常全面的指导开发者轻松入门 Cloud Native。 参考引用 原文同步至:https://waylau.com/why-cloud-native/ Cloud Native 分布式架构原理与实践
Cloud Native(云原生)是以云架构为优先的应用开发模式。目前,越来越多的企业已经在大规模开始拥抱云,在云环境开发应用、部署应用、发布应用。未来,越来越多的开发者也将采用 Cloud Native 来开发应用。本书是国内第一本 Java 领域 Cloud Native 著作。 那么为什么Cloud Native模式会越来越流行?Cloud Native与微服务有什么区别?何时选择使用Cloud Native?等等,这些问题将在本文一一解答。 什么是 Cloud Native Cloud Native (国内译为“云原生”),最早是 Matt Stine 提出的一个概念。与微服务一样,Cloud Native 并不是一种具体的技术,而是一类思想的集合,包括DevOps、持续交付(Continuous Delivery)、微服务(MicroServices)、敏捷基础设施(Agile Infrastructure)、康威定律(Conways Law)等,以及根据商业能力对公司进行重组。Cloud Native 既包含技术(微服务,敏捷基础设施),也包含管理(DevOps,持续交付,康威定律,重组等)。所以,Cloud Native 也可以说是一系列Cloud技术、企业管理方法的集合。 Cloud Native 具备有以下特性: 以云为基础架构 云服务 无服务 可扩展 高可用 敏捷 云优先 等等 下图是《Cloud Native 分布式架构原理与实践》书中所罗列的 Cloud Native 云架构模式。可见 Cloud Native 体系是非常庞杂的。 随着云计算的不断发展,企业开始采用基础架构即服务(IaaS)和平台即服务(PaaS)服务,并利用它们构建利用云的弹性和可伸缩性的应用程序,同时也能够满足云环境下的容错性。同时,云环境更加便宜和经济,因此,未来云环境会被作为企业部署、个人开发的优先选择。Cloud Native 的出现恰逢其时, 其架构可以指导企业或者个人轻松实现云应用开发或者云部署。 Cloud Native 与微服务的关系 在“简述 Microservices(微服务)”一文中,已经对微服务的概念做了简单的论述。 微服务架构风格其本质是把大的应用拆分成为小的服务(微服务)。微服务是单一应用的形式, 因此可以独立部署和运行在其自己的进程中。微服务一般采用轻量级的机制进行通信(一般是 HTTP 资源 API),因此可以不限制技术栈。微服务是围绕业务能力来构建,因此更加聚焦业务能力,能够把握住领域边界,放置需求的蔓延。微服务其固有的特性,方便通过全自动部署工具来实现独立部署,因此非常适合在云环境中进行部署。 在 Cloud Native 中,倾向于使用微服务来构建应用。同时,Cloud Native因为是以云环境为优先的,非常适合微服务的部署和管理。 目前,业界针对微服务有非常多的成熟方案,比如Spring Boot、Spring Cloud等,都可以简化微服务的开发工作。这微服务方面,笔者也撰写了一些列的免费教程(https://waylau.com/books/),读者朋友可以作为参考。 为什么我们需要使用 Cloud Native? 云计算的第一个浪潮是关于成本节约和业务敏捷性,尤其是云计算的基础设施更加廉价。 很多企业倾向于使用微服务架构来开发应用。微服务开发快速,职责单一,能够更快速的被客户所采纳。同时,这些应用能够通过快速迭代的方式,得到进化,赢得客户的认可。Cloud Native 可以打通微服务开发、测试、部署、发布的整个流程环节。 云供应商为迎合市场,提供了满足各种场景方案的 API,例如用于定位的 Google Maps,用于社交协作的认证平台等。将所有这些 API 与企业业务的特性和功能混合在一起,可以让他们为客户构建独特的方案。所有这些整合都在 API 层面进行。这意味着,不管是移动应用还是传统的桌面应用都能无缝集成。所以,采用 Cloud Native 所开发的应用都且具备极强的可扩展性。 软件不可能不出故障。传统的企业级开发方式,需要有专职人员来对企业应用进行监控与维护。而在 Cloud Native 架构下,底层的服务或者是 API 都由将部署到云中,等价于将繁重的运维工作转移给了云平台供应商。这意味着客户应用将得到更加专业的看护,同时,也节省了运维成本。 因此,云是大势所趋。快来拥抱Cloud Native! 如何实现 Cloud Native 那么如何来实现 Cloud Native 呢?其实这是一个非常大的话题,比如,作为开发者,你需要了解目前市面上流行的云供应商,了解微服务、SOA,了解 HTTP 和 REST,了解领域驱动设计(DDD),了解CICD和TDD,了解两个披萨,了解分布式的常用架构和模式等等。这里每一样都是一个庞大的课题,还好目前市面上已经有了一些资料可供学习,比如《Cloud Native 分布式架构原理与实践》,可以非常全面的指导开发者轻松入门 Cloud Native。 在本文的最后也列出了一些学习资料,读者有兴趣的话,可以由点及面,慢慢扩展自己的知识体系。 参考引用 原文同步至:https://waylau.com/about-cloud-native/ 简述 Microservices(微服务) Spring Boot 企业级应用开发实战 Spring Cloud 微服务架构开发实战 Cloud Native 分布式架构原理与实践 分布式系统常用技术及案例分析
本文介绍Sidecar模式的特点,及其应用的场景。熟悉Native Cloud或者微服务的童鞋应该知道,在云环境下,技术栈可以是多种多样的。那么如何能够将这些异构的服务组件串联起来,成为了服务治理的一个重大课题。而Sidecar模式为服务治理,提供了一种解决方案。 将应用程序的组件部署到单独的进程或容器中,以提供隔离和封装。此模式还可以使应用程序由异构组件和技术组成。 这种模式被称为Sidecar,因为它类似于连接到摩托车的边车。在该模式中,边车附加到父应用程序并为应用程序提供支持功能。 sidecar还与父应用程序共享相同的生命周期,与父项一起创建和退役。边车图案有时被称为搭接图案并且是分解图案。 问题背景 应用程序和服务通常需要相关的功能,例如监控、日志、集中化配置和网络服务等。这些外围任务可以作为单独的组件或服务来实现。 如果它们紧密集成到应用程序中,它们可以在与应用程序相同的进程中运行,从而有效地使用共享资源。但是,这也意味着它们没有很好地隔离,并且其中一个组件的中断可能会影响其他组件或整个应用程序。此外,它们通常需要使用与父应用程序相同的语言或者技术栈来实现。因此,组件和应用程序彼此之间具有密切的相互依赖性。 如果将应用程序分解为服务,则可以使用不同的语言和技术构建每个服务。虽然这提供了更大的灵活性,但这意味着每个组件都有自己的依赖关系,并且需要特定于语言的库来访问底层平台以及与父应用程序共享的任何资源。此外,将这些功能部署为单独的服务可能会增加应用程序的延迟。管理这些特定于语言的接口的代码和依赖关系也会增加相当大的复杂性,尤其是对于托管、部署和管理服务。 解决方案 上述问题的解决方案是,将一组紧密结合的任务与主应用程序共同放在一台主机(Host)中,但会将它们部署在各自的进程或容器中。这种方式也被称为“Sidecar(边车)模式”。 下图展示了任务与主应用程序的部署关系图。 Sidecar模式 边车服务不一定是应用程序的一部分,而是与之相关联。它适用于父应用程序的任何位置。Sidecar支持与主应用程序一起部署的进程或服务。这就像是如下图所示的边三轮摩托车那样,将边车安装在一辆摩托车上,就变成了边三轮摩托车。每辆边三轮摩托车都有自己的边车。类似同样的方式,边车服务共享其父应用程序的主机。对于应用程序的每个实例,边车的实例被部署并与其一起托管。 使用边车模式的优点包括: 在运行时环境和编程语言方面,边车独立于其主要应用程序,因此不需要为每种语言开发一个边车。 边车可以访问与主应用程序相同的资源。例如,边车可以监视边车和主应用程序使用的系统资源。 由于它靠近主应用程序,因此在它们之间进行通信时没有明显的延迟。 即使对于不提供可扩展性机制的应用程序,也可以使用边车通过将其作为自己的进程附加到与主应用程序相同的主机或子容器中来扩展功能。 Sidecar模式通常与容器一起使用,并称为边车容器。有关容器方面的内容,可以参阅https://waylau.com/ahout-docker/。 Sidecar模式的实现 Spring Cloud Netflix Sidecar框架提供了Sidecar模式的现成解决方案。Spring Cloud Netflix Sidecar框架框架可以提供对其他非Spring Cloud技术栈的微服务的治理。比如,你可以使用Node或者Golang编写一个Web项目,这个服务同样可以以Sidecar模式,纳入到Spring Cloud管理中去。 下面是实现步骤。 1. 为Web项目添加健康检查接口 提供REST接口,返回JSON格式内容{"status" : "up"}。其中status用于描述微服务的状态,常见的取值有UP、DOWN、OUT_OF_SERVICE和UNKNOWN等。 2. 编写Sidecar微服务 创建项目,添加Eureka、Sidecar的依赖: <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-netflix-sidecar</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-eureka</artifactId> </dependency> 启动类上加上@EnableSidecar注解。这是一个组合注解,它整合了三个注解,分别是@EnableCircuiBreaker和@EnableDiscoveryClient。 在配置文件中加入端口号、服务名称、Eureka地址以及Web项目的端口以及健康检查地址,如: server.port=8887 spring.application.name=sidecar-mylife-service eureka.client.serviceUrl.defaultZone=http://localhost:8881/eureka/ eureka.client.instance.prefer-ip-address=true sidecar.port=8080 sidecar.health-uri=http://localhost:8080/health eureka.instance.hostname=localhost 启动项目,并访问8887接口,就可以访问到Web项目中的接口。 3. Sidecar的一些端点 以下是Sidecar的常用端点: /hosts/{serviceId} 指定微服务在Eureka上的实例列表 /ping 返回OK字符串 /{serviceId} 请求对应的微服务 4. 部署应用 将Sidecar与Web服进行部署。一般是部署在相同的主机里面。 有关Spring Cloud的更多内容,可以参阅Spring Cloud 教程。 参考引用 原文同步至https://waylau.com/sidecar-pattern/ Spring Cloud 微服务架构开发实战:https://github.com/waylau/spring-cloud-microservices-development Spring Boot 企业级应用开发实战:https://github.com/waylau/spring-boot-enterprise-application-development https://docs.microsoft.com/en-us/azure/architecture/patterns/sidecar
2019年快来了,为了答谢各位关注老卫的读者、学员朋友,特送《Spring 5开发大全》三本,无套路,快上车。 时间的脚步总是匆匆,经历了一年的付出收获、欢乐忧伤,我们悄然间就要迈步到2019年了。 回顾这一年,最大的成就莫过于将Spring三剑客付梓出版。 何为Spring三剑客?即《Spring Boot 企业级应用开发实战》、《Spring Cloud 微服务架构开发实战》、《Spring 5 案例大全》,全方面覆盖从Java企业级到微服务的各个领域开发,是每个Java开发者的良师益友。 当然,书中的案例来自工资实践,来自于网友的讨论,更来自于这个开源的世界。不管是书里,还是老卫的博客里,每一篇教程的编写,我都满怀憧憬和希冀,每一个代码,都是一个相遇的音符。 2018年对我来说,是收获的一年。我收获了读者们的支持,给了我全新的人生体验。和你们的互动,让我感受到知识分享的快乐。你们对我的肯定,是我坚持的动力,所以谢谢大家。 2018年岁末,我的新书《Spring 5 案例大全》由北京大学出版社出版发行,在京东、当当等电商网站上陆续开始上市。这当然离不开你们的支持,为了感谢大家,特送出《Spring 5开发大全》三本,以回馈读者朋友们。 活动方式 愿意参加本次活动的朋友,请你们在该书的主页(https://book.douban.com/subject/30370024/),进行评价留言,讲述你我关于Spring的故事。届时将会挑选出三篇最佳留言,送上价值119元的新年礼物三份。绝对没有套路! Tips:打5星、长文字更容易中奖哦~ 活动时间 只要在2019年1月5日24时前的评价留言,都可以参与评选。 礼品介绍 礼品介绍可以见: 当当:http://product.dangdang.com/25581763.html 京东:https://item.jd.com/12474404.html
在前后台分离的应用中,Angular 与 Java 是一对好搭档。但是如果是分开部署应用,则势必会遇到跨域访问的问题。 什么是跨域 启动应用之后,有些浏览器会提示如下告警信息: No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://localhost:4200' is therefore not allowed access. 这个是典型的跨域问题。浏览器为了安全考虑,不同的域之间是不能够互相访问的的。 比如,Angular 应用部署在本地的4200端口,而 Java 服务部署在8080端口,他们虽然是同台机子,但仍然是不同的域。Angular 应用视图通过HttpClient 去访问 Java 的 http://localhost:8080/hello 接口是不允许的。 如何解决跨域问题 在知道了什么是跨域之后,解决方案就有多种。 1. 避免跨域 既然,分开部署导致了跨域,那么最简单的方式莫过于避免分开部署,即Angular 与 Java 同时部署到同个 Web 服务器中。 这种方式部署在传统的 Java Web 中非常常见。比如,JSP 应用。 但带来的问题是,水平扩展和性能调优将变得困难,不适合大型互联网应用。 2. 安装支持跨域请求的插件 其实,很多浏览器都提供了允许跨域访问的插件,只需启用这种插件,就能实现在开发环境跨域请求第三方 API 了。 下图展示的是在 Firefox 浏览器中能够实现的跨域访问的插件。 这种方式是最简单,但使用的场景比较受限,一般用于开发环境。 3. 设置反向代理 这种方式是业界最为常用的方式,原理是设置反向代理服务器,让 Angular 应用都访问自己的服务器中的API,而这类API都会被反向代理服务器转发到 Java 服务API中,而这个过程对于 Angular 应用是无感知的。 业界经常是采用 NGINX 服务来承担反向代理的职责。而在 Angular 中,使用反向代理将变得更加简单。在 Angualr 应用的根目录下,添加配置文件proxy.config.json,并填写如下格式内容: { "/lite": { "target": "http://localhost:8080", "secure": "false" } } 使用 Angular CLI 启动应用时,只需要执行如下命令即可,非常方法: ng serve --proxy-config proxy.config.json 这样,当 Angular 要访问http://localhost:4200/lite> 时,会被转发到 Java 的 接口。 参考引用 原文同步至https://waylau.com/angular-proxy/ Angular CLI 常用命令:https://waylau.com/angular-cli-commands/ Spring 5 开发大全:https://github.com/waylau/spring-5-book 跟老卫学Angular:https://github.com/waylau/angular-tutorial NGINX 教程:https://github.com/waylau/nginx-tutorial
有时,应用中需要一些比较新的依赖,而这些依赖并没有正式发布,还是处于milestone或者是snapshot阶段,并不能从中央仓库或者镜像站上下载到。此时,就需要 自定义Maven的<repositories>。 自定义Maven的<repositories> 以Spring应用程序程序为例,需要添加一个Spring Security 5.2.0.BUILD-SNAPSHOT版本的依赖,可惜这是个snapshot,并不在镜像站中。 解决方法就是在pom.xml添加一个snapshot的地址: <!-- Spring Snapshots仓库 --> <repositories> <repository> <id>spring-snapshots</id> <name>Spring Snapshots</name> <url>https://repo.spring.io/snapshot</url> </repository> </repositories> 这样,就能从这个仓库中,获取到 Spring Security 5.2.0.BUILD-SNAPSHOT版本的依赖了。 检查Maven的settings.xml 但有时,跟着上面的设置方法并不奏效,仍然还是从之前的镜像站中去下载。此时,我们需要去检查下 Maven 安装目录下的settings.xml: <mirror> <id>nexus-aliyun</id> <mirrorOf>*</mirrorOf> <name>Nexus aliyun</name> <url>http://maven.aliyun.com/nexus/content/groups/public</url> </mirror> 可以看到,在设置镜像时,mirrorOf设置为了“*”,意味所有的依赖都是从这个镜像上下载。完全不给Spring Snapshots机会了。 解决方法也简单,改为下面的方式: <mirror> <id>nexus-aliyun</id> <mirrorOf>*,!spring-snapshots</mirrorOf> <name>Nexus aliyun</name> <url>http://maven.aliyun.com/nexus/content/groups/public</url> </mirror> mirrorOf设置为了“*,!spring-snapshots”,意味除了Spring Snapshots外,所有的依赖都是从这个镜像上下载。这样就能下载到 Spring Snapshots了。 以下是mirrorOf的详细解释。 mirrorOf的配置解释 mirrorOf的配置支持如下场景: *:所有依赖; external:*:所有不再本地和不是基于文件的依赖; repo,repo1:所有来自repo或者repo1仓库的依赖; *,!repo1:除了repo1外的所有依赖。即上面示例中的场景。 参考引用 原文同步至https://waylau.com/customize-maven-repositories/
Angular CLI 是 Angular 客户端命令行工具,提供非常多的命令来简化 Angular 的开发。本文总结了在实际项目中经常会用到的 Angular CLI 命令。 获取帮助(ng -h) ng -h等同于ng --help,跟所有的其他命令行一样,用于查看所有命令的一个帮助命令。执行该命令可以看到 Angular CLI 所有的命令: >ng -h Available Commands: add Adds support for an external library to your project. build (b) Compiles an Angular app into an output directory named dist/ at the given output path. Must be executed from within a workspace directory. config Retrieves or sets Angular configuration values. doc (d) Opens the official Angular documentation (angular.io) in a browser, and searches for a given keyword. e2e (e) Builds and serves an Angular app, then runs end-to-end tests using Protractor. generate (g) Generates and/or modifies files based on a schematic. help Lists available commands and their short descriptions. lint (l) Runs linting tools on Angular app code in a given project folder. new (n) Creates a new workspace and an initial Angular app. run Runs a custom target defined in your project. serve (s) Builds and serves your app, rebuilding on file changes. test (t) Runs unit tests in a project. update Updates your application and its dependencies. See https://update.angular.io/ version (v) Outputs Angular CLI version. xi18n Extracts i18n messages from source code. For more detailed help run "ng [command name] --help" 创建应用 以下示例,创建一个名为“user-management”的 Angular 应用: ng new user-management 创建组件 以下示例,创建一个名为 UsersComponent 的组件: ng generate component users 创建服务 以下示例,创建一个名为 UserService 的服务: ng generate service user 启动应用 执行: ng serve --open 此时,应用就会自动在浏览器中打开。访问地址为 http://localhost:4200/。 升级依赖 目前,Angular 社区非常活跃,版本会经常更新。对 Angular 的版本做升级,只需简单一步执行: ng update 如果是想把整个应用的依赖都升级,则执行: ng update --all 自动化测试 Angular 支持自动化测试。Angular的测试,主要是基于Jasmine和Karma库来实现的。只需简单一步执行: ng test 要生成覆盖率报告,运行下列命令: ng test --code-coverage 下载依赖 光有 Angular 源码是否不足以将 Angular 启动起来的,需要先安装 Angular 应用所需要的依赖到本地。 在应用目录下执行: npm install 参考引用 更多有关 Angular 的内容,可以参阅《跟老卫学Angular》:https://github.com/waylau/angular-tutorial 原文同步至:https://waylau.com/angular-cli-commands/
Java 开发者对于 Spring 应该不会陌生。Spring 可以说是 Java EE 开发事实上的标准。无论是 Web 开发,还是分布式应用,Spring 都致力于简化开发者创建应用的复杂性。本文讨论 Spring 在狭义上以及广义上,所承载的不同的概念。 Spring 有广义与狭义之说。 狭义上的 Spring——Spring Framework 狭义上的 Spring, 是特指 Spring 框架(Spring Framework)。Spring 框架是为了解决企业应用开发的复杂性而创建的。Spring 框架的主要优势之一就是其分层架构。分层架构允许使用者选择使用哪一个组件,同时为 Java EE 应用程序开发提供集成的框架。Spring 框架使用基本的 POJO 来完成以前只可能由 EJB 完成的事情。然而,Spring 框架的用途不仅限于服务器端的开发。从简单性、可测试性和松耦合的角度而言,任何 Java 应用都可以从 Spring 框架中受益。Spring 框架的核心是控制反转(IoC)和面向切面(AOP)。简单来说,Spring 框架是一个分层的面向与 Java 应用的一站式轻量级开源框架。 Spring 框架前身,是 Rod Johnson 发表在 Expert One-on-One J2EE Design and Development 一书中所包含的3万行代码的附件。在书中,他展示了如何在不使用 EJB 的情况下构建高质量、可扩展的在线座位预留应用程序。为了构建该应用程序,他写了超过万行的基础结构代码。这些代码包含了许多可重用的 Java 接口和类,如 ApplicationContext 和 BeanFactory 等。由于 Java 接口是依赖注入的基本构件,因此他将类的根包命名为com.interface21,意思是这是一个提供给21世纪的参考!根据书中描述,这些代码已经在一些真实的金融系统中使用。 由于该书影响甚广,当时有几个开发人员 Juergen Hoeller 以及 Yann Caroff 联系上了 Rod Johnson,希望将com.interface21代码开源。Yann Caroff 将这个新框架并命名为了“Spring”,意思是就像一缕春风扫平传统 J2EE 的恶冬。所以说,Rod Johnson、Juergen Hoeller 以及 Yann Caroff 是 Spring 框架的共同创立者。 2003年2月,Spring 0.9 发布,采用了 Apache 2.0 开源协议。2004年4月,Spring 1.0 发布。到如今,Spring 框架已经是第5个主要版本了。 广义上的 Spring——Spring 技术栈 广义上的 Spring 是指以 Spring 框架为核心的 Spring 技术栈。这些技术栈涵盖了从企业级应用到云计算等各个方面的内容。包括: Spring Data:Spring 框架中的数据访问模块对 JDBC 及 ORM 提供了很好的支持。随着 NoSQL 和大数据的兴起,出现了越来越多的新技术,比如非关系型数据库、MapReduce 框架。Spring Data 正是为了让 Spring 开发者能更方便地使用这些新技术而诞生的“大”项目——它由一系列小的项目组成——分别为不同的技术提供支持,例如 Spring Data JPA、Sprng Data Hadoop、Spring Data MongoDB、Spring Data Redis 等等。通过 Spring Data,开发者可以用 Spring 提供的相对一致的方式来访问位于不同类型的数据存储中的数据。 Spring Batch:一款专门针对企业级系统中的日常批处理任务的轻量级框架,能够帮助开发者方便地开发出强壮、高效的批处理应用程序。通过 Spring Batch 可以轻松构建出轻量级的、健壮的并⾏处理应用,并支持事务、并发、流程、监控、纵向和横向扩展,提供统⼀的接口管理和任务管理。Spring Batch 对批处理任务进行了一定的抽象,它的架构可以大致分为三层,自上而下分别是业务逻辑层、批处理执行环境层和基础设施层。Spring Batch 可以很好地利用 Spring 框架所带来的各种便利,同时也为开发者提供了相对熟悉的开发体验。 Spring Integration:在企业软件开发过程中,经常会遇到需要与外部系统集成的情况,这时可能会使用 EJB、RMI、JMS 等各种技术,也许你会引入ESB。如果你在开发时用了 Spring 框架,那么不妨考虑下 Spring Integration——它为 Spring 编程模型提供了一个支持企业集成模式的扩展,在应用程序中提供轻量级的消息机制,可以通过声明式的适配器与外部系统进行集成。Spring Integraton 中有几个基本的概念:Message(带有元数据的Java对象)、Channel(传递消息的管道)和Message Endpoint(消息的处理端)。在处理端可以对消息进行转换、路由、过滤、拆分、聚合等操作;更重要的是可以使用 Channel Adapter,这是应用程序与外界交互的地方,输入是 Inbound、输出则是 Outbound,可选的连接类型有很多,比如 AMQP、JDBC、Web Services、FTP、JMS、XMPP、多种 NoSQL 数据库等。只需通过简单的配置文件就能将所有这些东西串联在一起,实现复杂的集成工作。 Spring Security:前身是 Acegi,是较为成熟的子项目之一,是一款可定制化的身份验证和访问控制框架。读者朋友如果对该技术有兴趣,可以参阅笔者所著的开源书《Spring Security 教程》(https://github.com/waylau/spring-security-tutorial)以了解更多 Spring Security 方面的内容。 Spring Mobile:对 Spring MVC 的扩展,旨在简化移动 Web 应用的开发。 Spring for Android:用于简化 Android 原生应用程序开发的 Spring 扩展。 Spring Boot:是 Spring 团队提供的全新框架,其设计目的是用来简化新 Spring 应用的初始搭建以及开发过程。该框架使用了特定的方式来进行配置,从而使开发人员不再需要定义样板化的配置。Spring Boot 为 Spring 平台及第三方库提供开箱即用的设置,这样你就可以有条不紊地来进行应用的开发。多数 Spring Boot 应用只需要很少的 Spring 配置。通过这种方式,Spring Boot 致力于在蓬勃发展的快速应用开发领域成为领导者。读者朋友如果对该技术有兴趣,可以参阅笔者所著的开源书《Spring Boot 教程》(https://github.com/waylau/spring-boot-tutorial)是了解更多 Spring Boot 方面的内容。本书的“第25章 Spring Boot”也会对 Spring Boot 做深入的探讨。 Spring Cloud:使用 Spring Cloud,开发人员可以开箱即用的实现分布式系统中常用的服务。这些服务可以任何环境下运行,包括分布式环境,也包括开发人员自己的笔记本电脑、裸机数据中心,以及 Cloud Foundry 等托管平台。Spring Cloud 基于 Spring Boot 来进行构建服务,并可以轻松地集成第三方类库,来增强应用程序的行为。读者如果对该技术有兴趣,可以参阅笔者所著的开源书《Spring Cloud 教程》(https://github.com/waylau/spring-cloud-tutorial)是了解更多 Spring Cloud 方面的内容。本书的“第26章 Spring Cloud”也会对 Spring Cloud 做深入的探讨。 ...... Spring 的技术栈还有很多,读者朋友有兴趣的话,可以访问笔者所著的“Spring 5案例大全”项目(<https://github.com/waylau/spring-5-book)了解更多信息。 约定 由于 Spring 是早期 Spring 框架的总称,所以,有时候这个“Spring”这个命名会给读者产生困扰。一般地,我们约定“Spring 框架”特指是狭义上的 Spring,即 Spring Framework;而“Spring”特指是广义上的 Spring,泛指 Spring 技术栈。 参考引用 原文同步至:https://waylau.com/what-is-spring/ 《Spring 5 开发大全》:https://book.douban.com/subject/30370024/
本文介绍了Java虚拟机(Java SE 11版本)加载类和接口。 加载类和接口 加载是指查找具有特定名称的类或接口类型的二进制形式的过程。典型的做法是,查找事先由Java编译器从源代码计算而来二进制表示,但也可能是通过动态计算。二进制形式最终会构造成一个Class对象。 加载的精确语义在Java Java Machine Specification,Java SE 11 Edition的第5章中给出。在这里,我们从Java编程语言的角度概述了该过程。 类或接口的二进制格式通常是上面引用的Java虚拟机规范Java SE 11版中描述的类文件格式,但只要满足第13.1节中规定的要求,其他格式也是可能的。 类ClassLoader的方法defineClass可用于从类文件格式的二进制表示构造Class对象。 表现良好的类加载器维护这些性质: 给定相同的名称,一个好的类加载器应该总是返回相同的类对象。 如果类加载器L1将类C的加载委托给另一个加载器L2,那么对于作为直接超类或C的直接超接口出现的任何类型T,或作为C中的字段类型,或作为类型方法的正式参数或C中的构造函数,或者作为C,L1和L2中方法的返回类型应该返回相同的Class对象。 恶意类加载器可能违反这些性质。但是,它不能破坏类型系统的安全性,因为Java虚拟机可以防范这种情况。 有关这些问题的进一步讨论,请参阅Java虚拟机规范,Java SE 11版和Java虚拟机中的动态类加载,作者:Sheng Liang和Gilad Bracha,作为ACO SIGPLAN发布的OOPSLA '98会议录。通告,第33卷,第10期,1998年10月,第36-44页。Java编程语言设计的基本原则是运行时类型系统不能被用Java编程语言编写的代码破坏,即使是这样的实现也是如此。否则敏感的系统类如ClassLoader和SecurityManager。 加载过程 加载过程由类ClassLoader及其子类实现。 ClassLoader的不同子类可以实现不同的加载策略。特别地,类加载器可以缓存类和接口的二进制表示,基于预期的使用来预取它们,或者将一组相关的类加载在一起。例如,如果找不到新编译的类,因为旧版本由类加载器缓存,这些活动可能对正在运行的应用程序不完全透明。但是,类加载器的责任是仅在程序中可能出现的情况下反映加载错误,而无需预取或组加载。 如果在类加载期间发生错误,那么将在程序中(直接或间接)使用该类型的任何点抛出类LinkichError的以下子类之一的实例: ClassCircularityError:无法加载类或接口,因为它将是自己的超类或超接口(第8.1.4节,第9.1.3节,第13.4.4节)。 ClassFormatError:声称指定所请求的编译类或接口的二进制数据格式错误。 NoClassDefFoundError:相关类加载器无法找到所请求的类或接口的定义。 因为加载涉及新数据结构的分配,所以它可能会因OutOfMemoryError而失败。 参考引用 原文同步至:https://waylau.com/jvm-loading-of-classes-and-interfaces/ Java 虚拟机规范(第11版)》:https://github.com/waylau/java-virtual-machine-specification
MyBatis虽然有很好的SQL执行性能,但毕竟不是完整的ORM框架,不同的数据库之间SQL执行还是有差异。笔者最近在升级 Oracle 驱动至 ojdbc 7 ,就发现了处理DATE类型存在问题。还好MyBatis提供了使用自定义TypeHandler转换类型的功能。 本文介绍如下使用 TypeHandler 实现日期类型的转换。 问题背景 项目中有如下的字段,是采用的DATE类型: birthday = #{birthday, jdbcType=DATE}, 在更新 Oracle 驱动之前,DateOnlyTypeHandler会做出处理,将 jdbcType 是 DATE 的数据转为短日期格式(‘年月日’)插入数据库。毕竟是生日嘛,只需要精确到年月日即可。 但是,升级 Oracle 驱动至 ojdbc 7 ,就发现了处理DATE类型存在问题。插入的数据格式变成了长日期格式(‘年月日时分秒’),显然不符合需求了。 解决方案: MyBatis提供了使用自定义TypeHandler转换类型的功能。可以自己写个TypeHandler来对 DATE 类型做特殊处理: /** * Welcome to https://waylau.com */ package com.waylau.lite.mall.type; import java.sql.CallableStatement; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.text.DateFormat; import java.util.Date; import org.apache.ibatis.type.BaseTypeHandler; import org.apache.ibatis.type.JdbcType; import org.apache.ibatis.type.MappedJdbcTypes; import org.apache.ibatis.type.MappedTypes; /** * 自定义TypeHandler,用于将日期转为'yyyy-MM-dd' * * @since 1.0.0 2018年10月10日 * @author <a href="https://waylau.com">Way Lau</a> */ @MappedJdbcTypes(JdbcType.DATE) @MappedTypes(Date.class) public class DateShortTypeHandler extends BaseTypeHandler<Date> { @Override public void setNonNullParameter(PreparedStatement ps, int i, Date parameter, JdbcType jdbcType) throws SQLException { DateFormat df = DateFormat.getDateInstance(); String dateStr = df.format(parameter); ps.setDate(i, java.sql.Date.valueOf(dateStr)); } @Override public Date getNullableResult(ResultSet rs, String columnName) throws SQLException { java.sql.Date sqlDate = rs.getDate(columnName); if (sqlDate != null) { return new Date(sqlDate.getTime()); } return null; } @Override public Date getNullableResult(ResultSet rs, int columnIndex) throws SQLException { java.sql.Date sqlDate = rs.getDate(columnIndex); if (sqlDate != null) { return new Date(sqlDate.getTime()); } return null; } @Override public Date getNullableResult(CallableStatement cs, int columnIndex) throws SQLException { java.sql.Date sqlDate = cs.getDate(columnIndex); if (sqlDate != null) { return new Date(sqlDate.getTime()); } return null; } } 如果是 Spring 项目,以下面方式进行 TypeHandler 的配置: <!-- 自定义 --> <!--声明TypeHandler bean--> <bean id="dateShortTypeHandler" class="com.waylau.lite.mall.type.DateShortTypeHandler"/> <!-- MyBatis 工厂 --> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource" /> <!--TypeHandler注入--> <property name="typeHandlers" ref="dateShortTypeHandler"/> </bean> 如何使用 TypeHandler 方式1 :指定 jdbcType 为 DATE 比如,目前,项目中有如下的字段,是采用的DATE类型: birthday = #{birthday, jdbcType=DATE}, 方式2 :指定 typeHandler 指定 typeHandler 为我们自定义的 TypeHandler: birthday = #{birthday, typeHandler=com.waylau.lite.mall.type.DateShortTypeHandler}, 源码 见https://github.com/waylau/lite-book-mall 参考引用 原文同步至https://waylau.com/mybatis-type-handler/
2022年12月
2022年11月
2022年10月
2022年06月
2022年05月
2022年04月
2022年03月
2022年01月
2021年12月
2021年09月