2024年3月

system design

https://github.com/donnemartin/system-design-primer

Performance
vs
scalability

scalability
这里面的伸缩性是指指标的。当系统有较高的负载时,每个用户仍然能够有较好的响应时,我们说他系统伸缩性强。

Performance
就是指一个请求的响应越快,自然说性能越高

如何才能让系统有更好的可伸缩性?

需要在系统设计初就提前考虑好系统设计,提前知道可能的问题点,性能点等等。

那么具体的系统设计如下:

Latency
vs
throughput

延时:单个请求的延时

吞吐量:单位时间内处理的请求量

Availability
vs
consistency

CAP理论:对于分布式系统,发生网络分区是一定要考虑的,因此在发生网络分区的情况下,对于C和A只能二选一。

当发生网络分区时,如果你允许用户操作,那么就会造成网络分区之间的数据不一致。

当发生网络分区时,如果你不允许用户操作,那么不同分区之间的数据不会因此而不一致,但这就不能保证可用性。

Consistency patterns

Weak consistency 通话视频,丢一些也没关系,最大限度保证通畅

Eventual consistency 可以忍受延时,保证最终一致性

Strong consistency 强一致性,比如Mysql 事务,事务执行后,第一次查询就要是最新的数据

Availability patterns

Fail-over : Active-passive 主从结构,当主节点挂掉后,从节点自动升级为主节点向外提供服务,比如redis哨兵,mysql主从。 Active-active 主主结构,集群中各个节点都向外提供服务,当某个节点挂机后,负载均衡器不将流量负载到宕机节点即可。比如一些分布式数据库,kafka集群等。

Availability in numbers

可靠性的一些指标,比如要达到几个9

99.9% availability - three 9s

Duration Acceptable downtime
Downtime per year 8h 45min 57s
Downtime per month 43m 49.7s
Downtime per week 10m 4.8s
Downtime per day 1m 26.4s

99.99% availability - four 9s

Duration Acceptable downtime
Downtime per year 52min 35.7s
Downtime per month 4m 23s
Downtime per week 1m 5s
Downtime per day 8.6s

一些常见的系统组件

DNS,用DNS做负载均衡的

CDN,某些云服务器厂商的CDN原理。我有一个域名keboom.site。然后我可以访问此域名拿到一些文件。在云厂商处配置keboom.site,他会返回给你一个类似 alicloud.keboom.site的域名,前端代码在请求资源时,使用 alicloud.keboom.site来获取资源。云厂商会将keboom.site 的资源缓存到他们的服务器上。前端程序将访问云厂商缓存的资源,这大概就是CDN原理。

负载均衡,平时我们说的有nginx,这其实是在application 层。对于负载均衡在网络协议中,layer 4 也是可以做的,我记得是有些硬件设备支持这种层次的协议。

数据库

关系式数据库,特点是他支持事务,关系型表结构。

一些讨论点: 主从:分主从的话,比如读写分离,那么就要有手段做读写分离 主主:数据冲突,事务性的破坏,需要控制访问哪个主

对于数据库主主,主从,共同的缺点:数据延时问题(主同步不及时,此时主挂机,则数据丢失),数据同步问题(同步数据,消耗性能) 联邦:国外是这么叫的,对于国内来说,他可能说叫根据业务进行数据切分。缺点:如果多数据库join就很麻烦。多数据库事务。

切片:数据做分区,切片。mysql是有partition功能的,可对数据做分区(但这只是mysql内部做的优化,如果数据量足够大,仅仅分区还是不够,可能还是要将数据分到多个数据库多个服务器,然后做hash)。当然有也人为的对表做切分,比如对表加后缀然后做hash,这样人为做切分。缺点很明显就是数据切分后,我们的查询逻辑变得复杂。

反规范:可以消除一些join,但带来一些冗余。

SQL优化:建表时:比如字段的选择,尽可能让表结构更小。SQL查询时,注意建立索引,并且能够使用到索引。慢查询排查等。


NoSQL

虽然一些NoSQL数据库声称自己支持事务,但是网络上并不太认同。

一般来说他们 可用性 > 一致性

key value:典型的redis等,作为分布式缓存使用。

doc:如MongoDB,文档型,数据结构更加灵活些,json类型的文档不必要每个字段都必须一致。如果你的数据结构层级较深,结构没有那么规整,那么可以考虑使用文档型

列式存储:大数据相关,HBase clickhouse等,特点是数据压缩效率高,支持大量的写入,大数据情形下表现好。比如日志存储,或者一些数据归档等,作为数据仓库等使用。

图形:社交关系

Cache

从头到尾,客户端缓存,CDN缓存,web sever缓存,application 缓存,分布式缓存,数据库缓存

cache pattern: 经典的redis 和 数据库的更新

查询时: cache aside:查询cache,如果cache没有,则从数据库中查询出来放到缓存中,然后返回。

缺点: 1.时间变长(查询缓存、更新数据库、更新缓存)2. 当数据库数据更新时,缓存中数据需要做过期或修改或删除等操作。3. 当缓存服务器重启,则所有缓存需要重新加载

更新时:

  1. 先更新数据库再跟新redis
  2. 先更新redis在更新数据库

以上两种方式都是有一定时间的数据不一致。

异步

消息队列—背压:生产者生产消息放到消息队列服务器的内存中,消费者进行消费时,直接从内存中读取。这样是效率最高的。如果队列中数据过多导致服务器内存不足,则消息势必存储到磁盘中。这时消费者再来进行消费,则需要先从磁盘中随机读,将数据加载到内存中,才能进行消费,这样速度就会慢。那么通过背压的方式,当内存满时,则同时生产者减慢或者暂停生产。

一、场景要求

我们在使用locust时,有时候默认的场景无法满足我们的要求时,这时后我们需要自定义场景

比如我们要设置每一段时间启动10个用户运行,执行60s后再一次启动10个用户,总共运行10分钟,默认的场景是无法满足这样的要求的,我们可以使用LoadTestshape类,LoadTestshape类提供了几种负载测试策略

二、用法

在脚本文件中定义一个类继承LoadTestshape类,locust在启动时发现文件中有使用这个类会自动启动。


在该类中需要定义tick()方法,该方法返回用户数以及产生率的元组(如果没有返回这两个测试将停止),locust启动后每秒调用一次该函数。


在LoadTestshape类中可以使用get_run_time()方法来获取测试运行的时间,使用此方法可以用来控制压测的总时间。

三、基于时间峰值策略

需求:比如我们要设置每一段时间启动10个用户运行,过一段时间后再一次启动10个用户,持续加压 60s

四、代码实现

import os
from locust import *


'''实现目标:每隔一段时间增加十个用户,实现持续加压'''


class CustomTaskSet(LoadTestShape):
# 设置压测时间60s
time_limit=60
#设置启动/停止的用户数
spawn_rate=10
def tick(self):
"""
返回一个元组,包含两值:
user_count -- 总用户数
spawn_rate -- 每秒启动/停止用户数
返回None时,停止负载测试
"""
#获取压测时间
run_time=self.get_run_time()
if run_time<self.time_limit:
#每隔一段时间启动10个用户;为-1时,表示将个位变为0,逢5进一
user_count=round(run_time,-1)
print(f'当前用户数{user_count},当前时间{run_time}')
return user_count,self.spawn_rate

return None

class IncrementalPressureMeasurement(HttpUser):
wait_time =between(1,2)
host="http://localhost:8080"
def on_start(self):

print("负载加压开始")

def on_stop(self):
print("负载加压结束")

@task
def increment_pressure(self):
self.client.post('/measurement',data={'measurement':''})


if __name__ == '__main__':
file_path = os.path.abspath(__file__)
os.system(f'locust -f {file_path} --web-host=127.0.0.1')
五、实现效果
最后我们欣赏下劳动成果吧,haha!

最后,还请大家可以点个免费的赞,你们的点赞才是我更新的动力!

 

看过很多保持MYSQL 与redis保持一致性的文章都提到了延迟删除,其实脱离任何业务场景的设计都是不切实际的,所以我会本着一个通用的读写场景去分析
为什么延迟删除大概率可以保证MYSQL与redis的最终一致。

通常的读写场景

通常在使用redis作为读写缓存时,我们采用的是
cache aside pattern
的形式,这种形式的读写一般是这样,

1,读请求: 从缓存中获取不到数据时,从db中读取数据,然后再set到缓存里。

2,写请求: 修改db中的数据,然后删掉缓存。

这样的模式大概率是不会有问题的,但是会有
极小的概率出现读请求中,可能会很长时间存在旧数据
,来看一下这个例子, 下面的数字标明了每个步骤执行的顺序。

可以看到在并发环境下,如果读请求先从db中读取了旧数据,然后写请求再去执行修改db,删除缓存的操作,此时读请求再把读取出来的旧db数据 set到缓存中,那么后续的读请求便会会一直读取到旧数据的缓存,除非缓存过期。

image.png

延迟删除是如何解决db与缓存数据不一致的

其实无论是上述缓存模型,还是其他的读写缓存方式,在并发环境下,其实都有可能出现缓存旧数据的问题,其
本质原因是,修改缓存的地方不是单协程进行的,多协程修改必然存在并发先后的问题

解决上述不一致的方式是可以延迟写请求中,删除缓存的时间。先来看下读写请求的并发场景,

1,如果读请求发生在写请求前,那么写请求后续删除缓存,是不存在不一致问题的。

2,如果读请求发生在写请求后,那么后续读请求读取出来的数据就是新数据,也不会有问题。

3,如果读写同时发生,但是缓存还未过期,此时也不会有一致性问题。

4,如果读写同时发生,但是缓存过期,这个时候才有可能出现上面缓存中长时间存在旧数据的问题。

基于此,我们可以延迟写请求中删除缓存的时间,因为一般我们数据有段比较长的缓存时间,在最开始读请求更新缓存后,后续的读请求会比较长时间读取缓存中数据,我们让写请求等待(可以同步,也可以异步)最开始更新缓存的读请求结束,然后再去删除缓存,就避开了读写请求并发更新缓存的场景,这样,下次读请求就不会读取到旧db数据缓存,而是重新从db中获取新数据。

具体等待多长时间才进行缓存删除,需要根据业务读请求接口的处理时长自己决定。

@

本篇将带你深入分析Elsa工作流原理,排除干扰展示关键代码段,以加深理解

定义

变量

Elsa工作原理可以抽象理解为管道中间件 + 异步模型

Elsa中,活动的变量的获取和设置都是异步的。Elsa定义了
Variable
类型作为异步操作的结果或者说是异步操作的占位符,这个变量在运行的时候才会填充数值。这与我们熟悉C#中的Task,或者js里的promise对象作用相同。输入Input,OutPut都属于 Variable。

Elsa模拟了内存寄存器(MemoryRegister)以及Set和Get访问器实现异步模型。

内存寄存器类

public class MemoryRegister
{
    ...
    public IDictionary<string, MemoryBlock> Blocks { get; }
    
}

寄存器中的存储区块类

public class MemoryBlock
{
    ...
    /// <summary>
    /// The value stored in this block.
    /// </summary>
    public object? Value { get; set; }
    
    /// <summary>
    /// Optional metadata about this block.
    /// </summary>
    public object? Metadata { get; set; }
}

变量到存储的映射类

Id可以代表变量在内存区块中的引用地址

public class MemoryBlockReference
{
    
    /// <summary>
    /// The ID of the memory block.
    /// </summary>
    public string Id { get; set; } = default!;

    public object? Get(MemoryRegister memoryRegister) => GetBlock(memoryRegister).Value;
}

构建活动时将创建活动中变量到存储区块的映射,分配一个引用给变量

 public void AssignInputOutputs(IActivity activity)
 {
     var activityDescriptor = _activityRegistry.Find(activity.Type, activity.Version) ?? throw new Exception("Activity descriptor not found");
     var inputs = activityDescriptor.GetWrappedInputProperties(activity).Values.Cast<Input>().ToList();
     var seed = 0;

     foreach (var input in inputs)
     {
         var blockReference = input?.MemoryBlockReference();

         if (blockReference != null!)
             if (string.IsNullOrEmpty(blockReference.Id))
                 blockReference.Id = $"{activity.Id}:input-{seed}";

         seed++;
     }
    ...
 }

异步变量获取和设置:

可以通过上下文对象的Set,和Get方法,异步获取和设置异步变量。

上下文对象

查看源码可以看到Elsa定义了如下Context

在这里插入图片描述

其中比较重要的上下文对象:

活动上下文(ActivityExecutionContext)

活动上下文对象由Elsa.Runtime提供,在工作流执行函数中可供访问。通过它可访问包含活动实例、当前输入和输出等。通过它可以访问当前活动所在的工作流执行上下文。

工作流执行上下文(WorkflowExecutionContext)

工作流上下文对象由Elsa.Runtime提供,可通过活动上下文(ActivityExecutionContext)访问其所属工作流执行上下文。通过它可访问包含工作流实例、当前活动、当前输入和输出等。

表达式执行上下文(ExpressionExecutionContext)

表达式执行上下文用于在构建活动时传递内存变量(输入,输出),其中包含MemoryRegister对象。

通过表达式执行上下文(ExpressionExecutionContext)获取到变量的值:
在这里插入图片描述

构建

构建活动

Elsa默认帮我们建立了这些活动:

在这里插入图片描述

在这里插入图片描述

他们都实现了IActivity接口,Activity和CodeActivity是IActivity的实现类,对应的是一个空的活动,(CodeActivity是带有自动完成功能的空活动)

我们要做的是继承这个活动,重写Execute方法以实现我们自己的业务。比如:

public class HelloWorld : Activity
{
    protected override async ValueTask ExecuteAsync(ActivityExecutionContext context)
    {
        Console.WriteLine("Hello World!");
        await CompleteAsync();
    }
}

以官方默认的WiteLine为例,这个类的Execute代码如下:

protected override void Execute(ActivityExecutionContext context)
{
    var text = context.Get(Text);
    var provider = context.GetService<IStandardOutStreamProvider>() ?? new StandardOutStreamProvider(System.Console.Out);
    var textWriter = provider.GetTextWriter();
    textWriter.WriteLine(text);
}

构建工作流

首要目标是拿到一个工作流对象(Workflow),Elsa启动时会从工作流提供者(IWorkflowProvider)获取所有能用的工作流。并注册到资源池中

public interface IWorkflowProvider
{
    string Name { get; }

    ValueTask<IEnumerable<MaterializedWorkflow>> GetWorkflowsAsync(CancellationToken cancellationToken = default);
}

Elsa默认的实现类是如下两种,BlobStorageWorkflowProvider将从数据库(BlobStorage)中反序列化来注册。ClrWorkflowProvider使用工作流构建器注册。

在这里插入图片描述

我们先定义工作流描述类,它继承自IWorkflow, WorkflowBase是IWorkflow的抽象基类

class SequentialWorkflow : WorkflowBase
{
    protected override void Build(IWorkflowBuilder workflow)
    {
        workflow.Root = new Sequence
        {
            Activities =
            {
                new WriteLine("Line 1"),
                new WriteLine("Line 2"),
                new WriteLine("Line 3")
            }
        };
    }
}
 

Elsa初始化时,WorkflowBuilder会构建程序集中所有实现IWorkflow的类。

WorkflowBuilder中的BuildWorkflowAsync方法会将工作流描述类IWorkflow对象构建成Workflow对象。

在这里插入图片描述

这里思考一个问题:终执行的代码是在活动中定义的,但为什么返回的是Workflow对象?通过代码研读,实际上Workflow也是一个IActivity活动,只不过它具有一个Root根节点的复合活动。活动的定义请参考
官方文档

BuildWorkflowAsync中的具体实现如下:

在这里插入图片描述

运行

注册

注册包括注册工作流和注册活动,配置Elsa时需要使用如下两个方法:

.AddActivitiesFrom<Program>()
.AddWorkflowsFrom<Program>()

注册工作流

工作流可以通过ClrWorkflowProvider,使用工作流构建器注册,也可以从本地存储(BlobStorage)中反序列化来注册。
代码构建的工作流是通过实现IWorkflow接口,在Elsa初始化时将工作流注册到工作流定义持久化到数据库的WorkflowDefinition表中

通过工作流构建器注册:

在这里插入图片描述

注册活动

Elsa使用描述器(IActivityDescriber)提供一个描述符(ActivityDescriptor),这里比较绕,阅读源码可以发现,其实是通过各种反射获取活动派生类的特征数据(有的系统喜欢将称之为元数据),封装这些数据的类型称之为描述符,特征数据可以作为在界面上显示,分组,排序的信息。

在这里插入图片描述

活动不同于工作流,它在运行中不持久化于数据库,而是以注册表的形式存储于内存中。

IDictionary<(string Type, int Version), ActivityDescriptor> _activityDescriptors

在构建工作流的时候自动注册活动,也可以通过实现IActivity接口,在Elsa初始化时将所有活动注册到注册表中
在这里插入图片描述

Elsa启动时将所有实现了IActivity接口的类型注册为活动:

在这里插入图片描述

填充

启动时填充活动注册表和工作流定义表。
官方也给出了说明,各填充两次确保活动注册表和工作流定义表都是最新的:

在这里插入图片描述

阶段一:填充活动注册表
因为工作流定义可以用作活动,需要确保在填充工作流定义表之前填充活动注册表。

阶段二:填充工作流定义表

阶段三:重新填充活动注册表
填充了工作流定义表之后,我们需要重新填充活动注册表,以确保活动描述符是最新的。

阶段四:用当前的活动集重新更新工作流定义表。
最后,需要重新填充工作流定义表,以确保工作流定义是最新的。

Invoke活动

Elsa默认的管道中间件:

在这里插入图片描述

Elsa注册执行活动的中间件(DefaultActivityInvokerMiddleware):

public static class ActivityInvokerMiddlewareExtensions
{
    /// <summary>
    /// Adds the <see cref="DefaultActivityInvokerMiddleware"/> component to the pipeline.
    /// </summary>
    public static IActivityExecutionPipelineBuilder UseDefaultActivityInvoker(this IActivityExecutionPipelineBuilder pipelineBuilder) => pipelineBuilder.UseMiddleware<DefaultActivityInvokerMiddleware>();
}

在这里插入图片描述

在执行活动的中间件(DefaultActivityInvokerMiddleware),最终活动被调用的代码如下:

在这里插入图片描述

可以看见,Elsa最终以反射的方式创建一个Activity实例,然后调用它的ExecuteAsync方法。

可观测性

设计器与APIs

实际上,Elsa的运行时和设计器是完全分离的。Elsa提供了一个基于Blazor的设计工具,它作为独立的项目发布在Github上:
Elsa-Studio

在这里插入图片描述

因为和接口交互是通过REST API实现的,所以你也可以使用任何你想要的客户端来实现。

接设计器默认的HTTP API实现在
Elsa.Workflows.Api
库中,用于支持设计器的增删改查业务。

如果仅要使用工作流引擎,可以使用
Elsa.Workflows.Management
库,它只包含对于工作流的管理而不涉及HTTP接口。

工作流配置

打开设计器,点击“工作流(Workflow)”菜单,然后单击“定义(Definition)”选项卡。可以看到一个工作流定义的列表。点击右上角新增按钮,

在这里插入图片描述

在打开的页面中,拖拽活动到工作流图上,然后单击“保存(Save)”按钮。

在这里插入图片描述

在浏览器的网络请求中可以看到一个POST请求,请求地址为/workflow/definitions,请求参数为JSON格式,后端服务中WorkflowDefinitions的Endpoint中将对编辑器的“保存”请求进行处理

在请求负载中,WorkflowDefinitionModel字段会包含工作流定义和Root活动。

默认实现会将工作流定义和根活动序列化为JSON,并将其保存到数据库中。其中根活动在数据库WorkflowDefinition表的StringData列中存储。

在这里插入图片描述

当工作流执行时,Elsa会实例化(Materialize)Workflow对象

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

其中RootActivity会反序列化,可以看到StringData会被反序列化为IActivity对象

在这里插入图片描述

在这里插入图片描述

查看工作流状态

Elsa定义了不同的接口和数据库

主要的接口如下:

workflowDefinition:工作流定义接口,数据来自WorkflowDefinition表
workflowInstance:工作流实例接口,数据来自WorkflowInstance表
activity-execution:活动执行接口,查询活动的Id、状态以及结果,输入输出等上下文数据,数据主要通过查询ActivityExecutionRecords表来获取。
journal: 活动执行日志,数据来自WorkflowExecutionLogRecords表

打开设计器,点击“工作流(Workflow)”菜单,然后单击“实例(Instance)”选项卡。可以看到一个工作实例列表

在这里插入图片描述

点击条目即可查看工作流的执行日志和各活动的执行信息。Web页面中各片区的数据来源分布大致如下:

在这里插入图片描述

其中页面中央的工作流编辑器显示了工作流的结构,结合工作流的执行日志,可以直观的看到工作流的执行情况。可观测到执行的步骤,以及执行的耗时。

--完结--

Java SE 22 新增特性

作者:
Grey

原文地址:

博客园:Java SE 22 新增特性

CSDN:Java SE 22 新增特性

源码

源仓库:
Github:java_new_features

使用未命名的变量和模式

我们经常需要定义一些我们根本不需要的变量。常见的例子包括异常、lambda表达式,例如:

try {
    String string = "xx";
    int number = Integer.parseInt(string);
} catch (NumberFormatException e) {
    System.err.println("Not a number");
}

如果异常变量 e 无须使用,那么上述例子中的变量 e 可以用 _ 代替

try {
    String string = "xx";
    int number = Integer.parseInt(string);
} catch (NumberFormatException _) {
    System.err.println("Not a number");
}

这个功能在 Java SE 21 中作为预览功能发布,详见
Java SE 21 新增特性
,在 Java 22 中通过 JEP 456 最终确定,不会有任何更改。

启动多文件源代码程序

自 Java 11 起,我们可以直接执行仅由一个文件组成的 Java 程序,而无需先对其进行编译,详见
Java SE 11 新增特性

例如,在 Hello.java 文件中保存一次以下 Java 代码:

public class Hello {
  public static void main(String[] args) {
    System.out.printf("Hello %s!%n", args[0]);
  }
}

不需要 javac 编译这个程序,而是可以直接运行它:

java Hello.java

我们也可以在 Hello.java 文件中定义多个类。但是,随着程序的增长,这种做法很快就会变得混乱;其他类应该定义在单独的文件中,并以合理的包结构组织起来。

然而,一旦我们添加更多的 Java 文件,Java 11 中所谓的 "启动单个文件源代码 "机制就不再起作用了。

比如定义两个类:

public class Hello {
  public static void main(String[] args) {
    System.out.println(Greetings.greet(args[0]));
  }
}
public class Greetings {
  public static String greet(String name) {
    return "Hello %s!%n".formatted(name);
  }
}

在 Java SE 11 中,无法执行,因为只支持单个 Java 文件运行,但是到了 Java SE 22,可以支持多个文件源码运行,比如上述两个类,在 Java SE 22 下,可以通过

java Hello.java

运行。

Foreign Function 和 Memory API


Project Panama
中,取代繁琐、易出错、速度慢的 Java 本地接口(JNI)的工作已经进行了很长时间。

在 Java 14 和 Java 16 中已经引入了 "外来内存访问 API "和 "外来链接器 API"--最初都是单独处于孵化阶段。在 Java 17 中,这些 API 被合并为 "Foreign Function & Memory API"(FFM API),直到 Java 18,它一直处于孵化阶段。

在 Java 19 中,
JDK Enhancement Proposal 424
最终将新的 API 提升到了预览阶段,

在 Java SE 22 中,外来函数与内存 API 终于由
JDK Enhancement Proposal 454
最终确定。

FFM API 可以直接从 Java 访问本地内存(即 Java 堆外的内存)和访问本地代码(如 C 库)。

下面是一个简单的例子,它在堆外内存中存储一个字符串,并对其调用 C 语言标准库的 "strlen "函数。

package git.snippets.jdk22;

import java.lang.foreign.*;
import java.lang.invoke.MethodHandle;


/**
 * FFM API
 * @since 22
 */
public class FFMTest {
    public static void main(String[] args) throws Throwable {
        // 1. Get a lookup object for commonly used libraries
        SymbolLookup stdlib = Linker.nativeLinker().defaultLookup();
        // 2. Get a handle to the "strlen" function in the C standard library
        MethodHandle strlen = Linker.nativeLinker().downcallHandle(stdlib.find("strlen").orElseThrow(), FunctionDescriptor.of(ValueLayout.JAVA_LONG, ValueLayout.ADDRESS));

        // 3. Get a confined memory area (one that we can close explicitly)
        try (Arena offHeap = Arena.ofConfined()) {

            // 4. Convert the Java String to a C string and store it in off-heap memory
            MemorySegment str = offHeap.allocateFrom("Happy Coding!");

            // 5. Invoke the foreign function
            long len = (long) strlen.invoke(str);
            System.out.println("len = " + len);
        }
        // 6. Off-heap memory is deallocated at end of try-with-resources
    }
}

本地化列表

Java SE 22 有了新的 ListFormat 类,我们就可以像在连续文本中一样,将列表格式化为枚举。

package git.snippets.jdk22;

import static java.text.ListFormat.*;

import java.text.ListFormat;
import java.util.List;
import java.util.Locale;

public class LocaleDependentListPatternsTest {
    void main() {
        List<String> list = List.of("Earth", "Wind", "Fire");
        System.out.println(ListFormat.getInstance(Locale.CHINA, Type.STANDARD, Style.FULL).format(list));
        System.out.println(ListFormat.getInstance(Locale.US, Type.STANDARD, Style.FULL).format(list));
        System.out.println(ListFormat.getInstance(Locale.GERMAN, Type.STANDARD, Style.FULL).format(list));
        System.out.println(ListFormat.getInstance(Locale.FRANCE, Type.STANDARD, Style.FULL).format(list));
    }
}

运行输出结果

Earth、Wind和Fire
Earth, Wind, and Fire
Earth, Wind und Fire
Earth, Wind et Fire

上述例子表明在不同的 Lacale 设置下,可自动根据配置进行格式化。

更多

Java SE 7及以后各版本新增特性,持续更新中...

参考资料

Java Language Changes for Java SE 22

JDK 22 Release Notes

JAVA 22 FEATURES(WITH EXAMPLES