开源通讯 2024年第一期

March 21, 2024

2024年1-3月份部分开源项目发布说明:

  • OpenJDK 22
  • Quarkus 3.8
  • Openshift 4.15
  • Keycloak 24
  • Kafka 3.7
  • ElasticSearch 8.12
  • Wildfly 31

OpenJDK 22

Oracle公司如期发布了Java 22,没有受到MacOS环境事件的影响

这个版本的JEP中,预览特性还是占有很大比例。参见下面列表,其中有Preview和Incubator是预览和孵化特性:

  • 423: Region Pinning for G1
  • 447: Statements before super(…) (Preview)
  • 454: Foreign Function & Memory API
  • 456: Unnamed Variables & Patterns
  • 457: Class-File API (Preview)
  • 458: Launch Multi-File Source-Code Programs
  • 459: String Templates (Second Preview)
  • 460: Vector API (Seventh Incubator)
  • 461: Stream Gatherers (Preview)
  • 462: Structured Concurrency (Second Preview)
  • 463: Implicitly Declared Classes and Instance Main Methods (Second Preview)
  • 464: Scoped Values (Second Preview)

我们重点看3个正式JEP,它们对Java软件开发生态建设有很大的推动作用:

1. JEP 454 外部函数和内存 API

关注Java的开发者应该对这个特性的缩写 FFM 很熟悉,其作用就是更好的和系统开发库(主要是C语言编写的)相交互。

在此之前,Java和C互相调用是通过JNI完成的。每个Java程序运行时,都会使用系统库,如libc等。 开发NIO网络,密码安全,高效内存访问等Java程序,开发者总是会考虑使用本地系统库来提高系统性能。 对于Java开发者而言,JNI开发和调试还是有一定难度门槛。另外,Java语言相比现代的一些语言,“胶水”能力不足,不太容易和本地软件库紧密协同工作。

JEP454 特性从名字上就能看出,更好的使用“外部”(针对Java虚拟机而言)的内存和函数。 我们用一个例子来说明。需要有一些基本的C语言编程知识。

JDK封装了一些技术感念:

  • 内存段(Memory segment)及其地址 - 一组用于处理本地内存及其指针的 API 类
  • 内存布局(Memory layout)和描述符 - 用于模拟外来类型(结构、基元)和函数描述符的应用程序接口
  • 链接器(Linker)和符号查询(symbol lookup) - 一组执行下调和上调的 API 类
  • 段分配器(Segment allocator) - 在内存会话中分配内存段的应用程序接口

第一步,找到本地函数

Linker linker = Linker.nativeLinker(); //本地链接器
SymbolLookup stdlibLookup = linker.defaultLookup(); //本地库符号查询
SymbolLookup systemLookup = SymbolLookup.loaderLookup(); //系统符号查询
SymbolLookup symbolLookup = name ->
        systemLookup.lookup(name).or(() -> stdlibLookup.lookup(name));

Optional<MemorySegment> printfMemorySegment = symbolLookup.lookup("printf"); //查找函数printf

printfMemorySegment 只是查询到函数的内存段。我们需要了解两个概念:

  • downcall: 是从高级子系统(例子中是 JVM)向低级子系统(如操作系统内核或本地代码)发起的事件。
  • upcall: 例如一些本地代码调用一些 Java 代码。

第二步,函数 downcall 获得 MethodHandle

import static java.lang.foreign.ValueLayout.ADDRESS;
import static java.lang.foreign.ValueLayout.JAVA_INT;

private static final FunctionDescriptor printfDescriptor = FunctionDescriptor.of(
    JAVA_INT.withBitAlignment(32), ADDRESS.withBitAlignment(64)
);

private static final MethodHandle printfMethodHandle = symbolLookup.lookup("printf").map(
    addr -> linker.downcallHandle(addr, printfDescriptor)
).orElse(null);

查询 printf 的参数,

$ man 3 printf

得到 int printf(const char *format, …),可以看到输入为字符指针,输出为int。

FunctionDescriptor则是描述printf的参数,JAVA_INT对应printf返回值int,ADDRESS对应传入参数,是一种值布局,其中对应的 C 类型是指向变量的指针,载体是MemoryAddress.class。

第三步,分配本地内存并调用

String str = "hello world";
try (Arena arena = Arena.ofConfined()) {
  MemorySegment cString = arena.allocateFrom(str);
  int ret = printfMethodHandle.invoke(cString);
}

当客户端使用堆外内存时,内存分配往往是一个瓶颈。因此,FFM API 包含一个SegmentAllocator抽象,用于定义分配和初始化内存段的操作。 为了方便起见,Arena 类实现了SegmentAllocator接口,这样就可以使用 Arena 从各种现有资源分配本地段。换句话说,Arena是灵活分配和及时取消堆外内存的 “一站式商店”。

Arena 还实现了 AutoClosable 接口,使用 try-with-resources 结构大大简化了内存回收操作。

限于篇幅,这个例子只能简要介绍FFM API。详细技术内容请参见 Panama 项目。

2. JEP 456 未命名变量和模式

这个特性简单描述,就是用下划线 “_” 来占位,而不用写用不着的代码符号。 见下面的代码例子:

static int count(Iterable<Order> orders) {
    int total = 0;
    for (Order order : orders)    // order 没有用到
        total++;
    return total;
}

我们经常在看Java代码时,会发现在函数中有一些被定义,但没有用到的变量,如上面的order,因为我们只关心数量而不是具体每个order。

这时 order 的出现,会带来负担,开发者阅读时会留意并疑惑,即使有IDE帮助(上述代码会变灰色或者标注未被使用),还是有维护成本。 我们可以用 _ 来代替:

static int count(Iterable<Order> orders) {
  int total = 0;
  for (Order _ : orders)    // Unnamed variable 未命名变量
    total++;
  return total;
}

Python,Scala等语言早已是这样的用法,占位而不使用。

对于 try-catch 也是非常方便

try { ... }
catch (Exception _) { ... }   // Unnamed variable
catch (Throwable _) { ... }   // Unnamed variable

我觉得特别有用的还有在多层嵌套结构中,使用下划线是的结构清晰多了

if (r instanceof ColoredPoint(Point(int x, _), _)) { ... x ... }

不过有个地方要留意,两个下划线 “__",还是一个合法的变量定义。

3. JEP 458 启动多文件源代码程序

在 JDK 11 中,JEP 330 增强了 java 应用程序启动器,使其能够直接运行 .java 源文件,而无需明确的编译步骤。例如,假设文件 Prog.java 声明了两个类:

class Prog {
    public static void main(String[] args) { Helper.run(); }
}
class Helper {
    static void run() { System.out.println("Hello!"); }
}

运行

$ java Prog.java

在内存中编译两个类,并执行该文件中声明的第一个类的 main 方法。

这种运行程序的低级方法有一个很大的局限性: 程序的所有源代码都必须放在一个 .java 文件中。要使用多个 .java 文件,开发人员必须重新明确编译源文件。

本JEP增强了 java 启动器的源文件模式,使其能够运行以多个 Java 源代码文件形式提供的程序。

例如,假设一个目录包含两个文件:Prog.java 和 Helper.java,其中每个文件都声明了一个类:

// Prog.java
类 Prog {
    public static void main(String[] args) { Helper.run(); }
}
// Helper.java
类 Helper {
    static void run() { System.out.println("Hello!"); } // Helper.java class Helper {
}

运行 java Prog.java 会在内存中编译 Prog 类并调用其 main 方法。由于该类中的代码引用了 Helper 类,启动器会在文件系统中找到 Helper.java 文件,并在内存中编译该类。如果 Helper 类中的代码指向其他类,例如 HelperAux,那么启动器也会找到 HelperAux.java 并编译它。

当不同 .java 文件中的类相互引用时,java 启动器不保证 .java 文件编译的任何特定顺序或时间。例如,启动器有可能先编译 Helper.java,然后再编译 Prog.java。某些代码可能会在程序开始执行前编译,而其他代码则可能会在运行过程中晚点再编译。

这个特性看起来不起眼,但我觉得意义重大,java正在向其他交互性解释语言学习(如Python),不断提升使用命令行交互编程的能力。 现在有了jshell,支持shebang能力,支持单个以及多个java文件的直接解释执行,相信很快类似IPython这样的好用的编程工具就会出现。 再加上 FFM API 等便捷访问本地运算库的能力,Java就可以更好的执行数据科学计算的任务。

Quarkus 3.8

2月份发布的 Quarkus 3.8 版本是一个LTS长期支持版本。目前Quarkus在版本2之后发布了2.6,3.2等LTS,维护期为一年。比起运行时长达3-5年的LTS维护期,开发工具一年似乎短了一些,不过这也是技术快速迭代的一种体现吧。

和上个LTS相比,主要特性变化有:

  • OpenTelemetry 功能提升
  • Reactive Messaging Pulsar 扩展
  • 支持 Redis 7.2
  • 支持 Flyway
  • Java 17 作为基准,并支持Java 21
  • 安全 OIDC 加强
  • Hibernate ORM 6.4支持

Quarkus目前已经是完备的Java云原生开发框架,多数Java三方库都提供SpringBoot和Quarkus的支持。 积极对AI等新技术提供集成能力,骨子里的AOT提前编译生成思想,非常和目前的生成式模型思路契合。

Openshift 4.15

OpenShift 4.15 基于 Kubernetes 1.28 和 CRI-O 1.28。

作为庞大的系统平台,新版本提供的特性和变动实在是太多了。 这里只列出对开发者重要的变化:

  • 控制台提供对 Red Hat Developer Hub 的集成,Developer Hub用 Backstage实现,是一个开发者门户
  • Pipelines 提供了基于插件的全新动态仪表板,基于Tekton 0.56 项目
  • GitOps能力,通过集成Argo CD 2.10
  • 创建无服务器,采用 Knative 1.11
  • 更新ServiceMesh,基于 Istio 1.18 和 Kiali 1.73
  • 监控能力,采用OpenTelemetry 3.1,利用收集器获取数据
  • 提供了应用迁移工具 MTA 7.0,帮助应用软件迁移到云原生环境下,即业内提出的软件“现代化”
  • 主控程序使用的系统RHCOS,使用RHEL 9.2操作系统
  • 基于Shipwright的构建平台,在K8S上构建容器镜像,统一支持目前常见的技术,如 Kaniko,Buildpacks,Buildah等

Keycloak 24

这个大版本主要特性是,支持用户配置文件,并可以渐进式配置。

作为一个SSO软件,用户可以登录,对自己的信息进行维护修改,一个友好使用方便的界面非常重要。 Keycloak 通过支持用户配置特性,方便开发者定制用户的属性和界面。以下是文档说明:

  • 对用户和管理员可管理的属性进行细粒度控制
  • 能够指定管理哪些用户属性,并在表单上给普通用户或管理员显示
  • 动态表单 - 以前添加任何属性都需要创建自定义主题。现在不需要了,用户可以根据特定部署看到所需的属性
  • 验证 - 可为用户属性指定验证器,包括内置验证器,用于指定最大或最小长度、特定 regex 或将特定属性限制为 URL 或数字
  • 注解 - 可指定特定属性的呈现方式,例如文本区域、带有指定选项的 HTML 选择、日历或许多其他选项
  • 渐进式配置 - 可在表单中指定某些字段为必填字段或仅对特定范围参数值可用的字段,用户根据其应用程序要求逐步填写属性

另外通过freemarker模板来渲染用户定制页面

在这个版本中,Java Adapter都标注为Deprecated,其中Jetty Adapter被移除,其他Java服务器Adapter,如Tomcat, SpringSecurity, Wildlfy, Servlet等Adapter还保留,但不推荐新应用开发使用。未来会逐步移除,具体技术说明参考 https://www.keycloak.org/2022/02/adapter-deprecation.html。 Java应用,包括SpringBoot,集成Keycloak,推荐使用Wildfly安全组件Elytron OIDC模块。

因为这个技术变化,网上的很多Keycloak文章,已经“过时”了。这部分解释清楚需要花些时间,如果有兴趣留言联系进行技术交流。

这个版本支持了OAuth2.1 和 轻量级访问令牌,提升了信任仓库(Truststore)的功能,网络连接、mTLS、数据库驱动等都使用一份信任仓库即可。

Kafka 3.7

这个版本是一个承上启下的版本,我们先看一下发布说明中的重点:

JBOD

发布 KRaft 中 JBOD 的早期访问版本,详情请参见KIP-858。

JBOD(Just a Bunch Of Disks)即每个broker多个日志目录,这个特性Kafka很早就支持,是一项重要功能,允许 Kafka 在每个 broker 在有多个存储设备的大型部署中运行。 为确保可用性,当分区领导者出现故障时,控制器应从其他同步副本中选出一个新的领导者,但控制器不会检查每个领导者是否正确履行职责。控制器就会简单地认为broker工作正常,只要它集群的活跃成员。 在 KRaft 中,集群成员资格会保持,只要每个broker向活动控制器发送的及时心跳请求。而在 ZooKeeper 中,集群成员资格基于/brokers/ids 下的瞬态 zNode。

在 KRaft 模式中,当单个日志目录发生故障时,其broker将无法成为任何分区的领导者或跟随者。控制器不会收到信号,提醒需要更新副本的leadship和 ISR ,而broker将继续发送心跳请求。 在 ZooKeeper 模式下,当某个日志目录发生故障时,broker会向控制器发送通知,然后控制器会向broker发送完整的LeaderAndIsr请求,列出该broker所有日志目录的所有分区。

控制器依靠来自broker的每个分区错误结果来更新故障日志目录中副本的leadship和 ISR。如果没有该通知,该日志目录上具有leadship的分区将不会获得新领导的分配,并将保持不可用状态。 KRaft 对JBOD提供支持,通过从broker到控制器的新 RPC,名称为AssignReplicasToDirs,在失败的日志目录中显示受影响的主题分区。

Kafka社区已经在开始准备4.0版本,提出很多项功能注意点:

  • ZooKeeper 自 3.5.0 版起被标记为弃用。计划在 Apache Kafka 4.0 中移除 ZooKeeper。
  • 在 Apache Kafka 2.1 之前发布的客户端 API 现已在 3.7 中标记为过时,并将在 Apache Kafka 4.0 中移除。
  • Java 11 对 Kafka 代理的支持在 3.7 中也被标记为弃用,并计划在 Kafka 4.0 中移除。

分级存储

这里补充说明下 Kafka 3.6 提供的新功能 Tiered Storage 分级存储。

Kafka 将消息存储在 Kafka Broker 的本地磁盘上的不断添加的日志文件中。保留期基于 “log.retention”,可以进行设置。保留期为用户提供了数据保证,即使应用程序出现故障或因维护而停机,也能在保留期内读取之前的数据,而不会丢失任何数据。 Kafka 集群所需的总存储量与主题/分区的数量、消息传输速率以及保留期成正比。Kafka 通常需要大量磁盘存储空间,总存储容量可达数10TB。 Kafka 数据主要是通过尾部流式读取方式,利用操作系统的页面缓存来提供数据,而不是磁盘读取。较旧的数据通常是从磁盘读取的,用于回填或故障恢复,而且并不频繁。

在分层存储方法中,Kafka 集群配置了两层存储 - 本地层和远程层。本地层与目前 Kafka 采用的相同,使用 Kafka Broker 服务器上的本地磁盘来存储日志段。 新的远程层使用 HDFS 或 S3 等系统来存储已完成的日志段。每个层都定义了两个独立的保留期。

启用远程层后,本地层的保留期可以从几天大幅缩短到几小时。远程层的保留期可以更长,几天甚至几个月。日志段在本地层滚动时,会连同相应的索引一起复制到远程层。 对延迟敏感的应用程序会执行尾部读取,并从本地层利用现有的 Kafka 机制高效地使用页面缓存来提供数据。从故障中恢复的回填和其他应用需要的数据比本地层中的数据更早的,则由远程层提供。

这种分层方式还有另一个巨大的优点,就是目前公有云平台存储也有分级,比如AWS的存储,针对不同的访问频率,可以租用不同的存储,价格可能会有10倍即一个数量级,对于大型应用来说,能减少客观的成本。

这个特性还处在早期访问阶段,红帽等公司也参与贡献。我们会积极关注技术动向,帮助客户技术提升。有感兴趣的朋友可以联系交流。

ElasticSearch 8.12

这个版本基于 Apache Lucene 9.9,速度提升,开发人员可以定制搜索过程,代码简洁易于维护。

重点特性有:

  • 支持BM25、矢量搜索、语义搜索,以及上述技术的混合组合
  • 利用标量量化和融合乘加 (FMA),降低成本和查询延迟,提高矢量数据搜索的摄取性能
  • 利用查询并行化大幅提高搜索和聚合速度
  • 直接在控制面板上编辑 ES|QL 查询,无需在控制面板和发现应用程序之间切换,并利用 ES|QL 应用程序内文档的改进帮助用户快速学习

现在AI技术中,一个很火热的技术话题是RAG(Retrieval Augmented Generation),Elastic在之前的版本就已经提供对于向量数据查询的支持。 RAG实现与结构良好、优化的数据密切相关。在集成目录中,为更好的搜索体验,使用本地连接器构建正确的上下文。

原生连接器是托管在 Elastic Cloud 上,通过配置就可以集成。这个版本提供了以下连接器:

  • AWS S3
  • 谷歌云存储
  • Salesforce
  • Oracle
  • Azure Blob 存储
  • MongoDB
  • MySQL
  • Postgres
  • MSSQL

Wildfly 31

压轴出场的是Wildfly 31,里面的 Glow 特性非常惊艳。

WildFly Glow CLI,是一个工具,扫描部署war包文件,生成 WildFly Server、WildFly Bootable JAR 或 docker 镜像等运行时。 目前我们习惯采用可运行jar包,其实是嵌入了Servlet容器或者Web服务器运行程序的。之前使用war包部署的方式,只有业务代码和必要的三方库。springboot等微服务方案,其实就是在war基础之上加入了所有必要的三方运行库和Web服务器。但是,jar是个微服务,但不是完成的服务器程序。

在一些场景下,我们还是需要完整的服务器功能的,比如用户管理,安全设置,数据库配置,运维监控等。微服务需要另外的服务器治理工具,其实给客户带来更多的技术成本和要求。完全可以利用上述思路,生成微服务、完整的服务器、或者容器运行时。这就是Glow工具的思路。

Glow 是 “Galleon Layers Output from War “的缩写。通过探查部署文件,WildFly Glow 可以分析确定应用程序所需的 Galleon 功能包和层集。 比如应用程序中用到了JaxRS和JPA接口,Glow知道用到这些API接口,已经对应的实现,Wildfly中是Resteasy和Hibernate。在打包生成的服务器中,只有用到的三方包会被包含进来,并且只有相应功能的配置文件。

尽管 WildFly Glow 名称的缩写中使用了 “War”,但可以分析所有 Jakarta EE 部署类型,包括Jar包,而不仅仅是 .war 文件。也就是说,Spring框架的应用也适用。

WildFly Glow 基于 WildFly Galleon 图层中的规则。Galleon 是一款配置工具,用于创建和维护由一个或多个产品(或组件)组成的软件发行版。

示例,比如有一个 myapp.war,大致知道采用了一些JEE技术,以及使用了pg数据库,我们希望能够基于最新的服务器组件集合生成可运行程序:

wildfly-glow.sh scan myapp.war --add-ons=postgresql --provision=SERVER
  • –add-ons=postgresql 表示采用pg插件
  • –provision=SERVER 表示打包成可运行的服务器,还可以是BOOTABLE_JAR(可运行jar包),DOCKER_IMAGE(容器镜像)

glow会扫描war包,得出元数据信息

context: bare-metal
enabled profile: none
galleon discovery
- feature-packs
   org.wildfly:wildfly-galleon-pack:30.0.1.Final
- layers
   ee-core-profile-server
   jpa
   ejb-lite
   jaxrs
   jsf
   postgresql-driver

layers即war包中的JEE技术栈。经过一段时间生成

Provisioning server...
Resolving feature-packs
Installing packages
Resolving artifacts
...
Generating configurations
Delayed generation, waiting...
Copy ./examples/kitchensink.war to server-31.0.0.Final/standalone/deployments/kitchensink.war
Provisioning DONE.
To run the server call: 'sh server-31.0.0.Final/bin/standalone.sh'

现在我们得到了一个已经部署应用的完整服务器,而这个服务器的组件都全新的!不用担心有安全漏洞。

如果您的应用是采用比较标准的API开发,则迁移升级应用会非常方便。对于国内还在广泛使用的JDK8甚至JDK6的Java应用程序,是一个值得尝试的技术方法。

Wildfly 31 其他重要的特性还有:

  • 支持 MicroProfile 6.1
  • 支持 Jakarta MVC 2.1
  • Reactive Messaging 对 AMQP 协议的支持
  • 加入了稳定级别(Stability levels)的划分,更好的管理子系统成熟度
  • Configuration配置项导出