2024年11月

简介

.NET通过委托来提供回调函数机制,与C/C++不同的是,委托确保回调是类型安全,且允许多播委托。并支持调用静态/实例方法。

简单来说,C++的函数指针有如下功能限制,委托作为C#中的上位替代,能弥补函数指针的不足。

  1. 类型不安全
    函数指针可以指向一个方法定义完全不同的函数。在编译期间不检查正确性。在运行时会导致签名不同导致程序崩溃
  2. 只支持静态方法
    只支持静态方法,不支持实例方法(只能通过邪道来绕过)
  3. 不支持方法链
    只能指向一个方法定义

函数指针与委托的相似之处

函数指针

typedef int (*func)(int, int);

委托

delegate int func(int a, int b);

委托底层模型

image

delegate关键字作为语法糖,IL层会为该关键字自动生成Invoke/BeginInvoke/EndInvoke方法,在.NET Core中,不再支持BeginInvoke/EndInvoke

眼见为实

    public abstract partial class Delegate : ICloneable, ISerializable
    {
        // _target is the object we will invoke on
        internal object? _target; // 源码中的注释不太对(null if static delegate)。应该是这样:如果注册的是实例方法,则是this指针,如果是静态则是delegate实例自己。

        // MethodBase, either cached after first request or assigned from a DynamicMethod
        // For open delegates to collectible types, this may be a LoaderAllocator object
        internal object? _methodBase; //缓存

        // _methodPtr is a pointer to the method we will invoke
        // It could be a small thunk if this is a static or UM call
        internal IntPtr _methodPtr;//实例方法的入口,看到IntPtr关键字就知道要与非托管堆交互,必然就是函数指针了,

        // In the case of a static method passed to a delegate, this field stores
        // whatever _methodPtr would have stored: and _methodPtr points to a
        // small thunk which removes the "this" pointer before going on
        // to _methodPtrAux.
        internal IntPtr _methodPtrAux;//静态方法的入口
    }
    public abstract class MulticastDelegate : Delegate
    {
		//多播委托的底层基石
        private object? _invocationList; 
        private nint _invocationCount;

		//实例委托调用此方法
		private void CtorClosed(object target, IntPtr methodPtr)
        {
            if (target == null)
                ThrowNullThisInDelegateToInstance();
            this._target = target;
            this._methodPtr = methodPtr;//函数指针被指向_methodPtrAux
        }
		//静态委托调用此方法
		private void CtorOpened(object target, IntPtr methodPtr, IntPtr shuffleThunk)
        {
            this._target = this;//上面说到,_target的注释不对的判断就在此
            this._methodPtr = shuffleThunk;//与实例委托不同,这里被指向一个桩函数
            this._methodPtrAux = methodPtr;//函数指针被指向_methodPtrAux
        }
    }
	

委托如何同时支持静态方法与实例方法?

示例代码
        static void Main(string[] args)
        {
            //1.注册实例方法
            MyClass myObject = new MyClass();
            MyDelegate myDelegate2 = new MyDelegate(myObject.InstanceMethod);
            myDelegate2.Invoke("Hello from instance method");

            Debugger.Break();

            //2.注册静态方法
            MyDelegate myDelegate = MyClass.StaticMethod;
            myDelegate.Invoke("Hello from static method");

            Debugger.Break();

        }
    }

    public delegate void MyDelegate(string message);

    public class MyClass
    {
        public static void StaticMethod(string message)
        {
            Console.WriteLine("Static Method: " + message);
        }

        public void InstanceMethod(string message)
        {
            Console.WriteLine("Instance Method: " + message);
        }
    }
            myDelegate2.Invoke("Hello from instance method");
00007ff9`521a19bd 488b4df0        mov     rcx,qword ptr [rbp-10h]
00007ff9`521a19c1 48baa0040000c7010000 mov rdx,1C7000004A0h ("Hello from instance method")
00007ff9`521a19cb 488b4908        mov     rcx,qword ptr [rcx+8]
00007ff9`521a19cf 488b45f0        mov     rax,qword ptr [rbp-10h]
00007ff9`521a19d3 ff5018          call    qword ptr [rax+18h] //重点
00007ff9`521a19d6 90              nop   
            myDelegate.Invoke("Hello from static method");
00007ff9`521a1a54 488b4de8        mov     rcx,qword ptr [rbp-18h]
00007ff9`521a1a58 48baf0040000c7010000 mov rdx,1C7000004F0h ("Hello from static method")
00007ff9`521a1a62 488b4908        mov     rcx,qword ptr [rcx+8]
00007ff9`521a1a66 488b45e8        mov     rax,qword ptr [rbp-18h]
00007ff9`521a1a6a ff5018          call    qword ptr [rax+18h]  //重点
00007ff9`521a1a6d 90              nop

可以看到,静态与实例都指向了rax+18h的地址偏移量。那么+18到底指向哪里呢?
image

Invoke的本质就是调用_methodPtr所在的函数指针

那么有人就会问了,前面源码里不是说了。静态方法的入口不是_methodPtrAux吗?怎么变成_methodPtr了。
实际上,如果是静态委托。JIT会生成一个桩方法,桩方法内部调用会+20偏移量的内容。从而调用_methodPtrAux

实例与静态核心代码的差异,大家有兴趣的话可以看一下它们的汇编

  1. 实例方法核心代码
private void CtorClosed(object target, nint methodPtr)
{
	if (target == null)
	{
		ThrowNullThisInDelegateToInstance();
	}
	_target = target;
	_methodPtr = methodPtr;//_methodPtr真正承载了函数指针
}

  1. 静态方法核心代码
private void CtorOpened(object target, nint methodPtr, nint shuffleThunk)
{
	_target = this;
	_methodPtr = shuffleThunk;//_methodPtr只是一个桩函数
	_methodPtrAux = methodPtr;//真正的指针在_methodPtrAux中
}

委托如何支持类型安全?

点击查看代码
    internal class Program
    {
        static void Main(string[] args)
        {
            //1. 编译器层面错误
            //var myDelegate = new MyDelegate(Math.Max);

            //2. 运行时层类型转换错误
            var myDelegate = new MyDelegate(Console.WriteLine);
            MyMaxDelegate myMaxDelegate = (MyMaxDelegate)(object)myDelegate;

            Debugger.Break();
        }

        public delegate void MyDelegate(string message);
        public delegate int MyMaxDelegate(int a, int b);


    }
  1. 编译器层会拦截
    这个很简单,在编译器中如果定义不匹配就会报错。
    image

  2. CLR Runtime会在汇编中插入检查命令
    检查不一致会报错,不至于整个程序奔溃。
    image

委托如何支持多播?

image

多播委托的添加

委托使用+=或者Delegate.Combine来添加新的委托。其底层调用的是CombineImpl,由子类MulticastDelegate实现。
并最终产生一个
新的委托

for循环1000次Combine委托,会产生1000个对象,

		//简化版
        protected sealed override Delegate CombineImpl(Delegate? follow)
        {
            MulticastDelegate dFollow = (MulticastDelegate)follow;
            object[]? resultList;
            int followCount = 1;
            object[]? followList = dFollow._invocationList as object[];
            if (followList != null)
                followCount = (int)dFollow._invocationCfollowListount;

            int resultCount;
			            if (!(_invocationList is object[] invocationList))
            {
                resultCount = 1 + followCount;
                resultList = new object[resultCount];
                resultList[0] = this;
                if (followList == null)
                {
                    resultList[1] = dFollow;
                }
                else
                {
                    for (int i = 0; i < followCount; i++)
                        resultList[1 + i] = followList[i];
                }
                return NewMulticastDelegate(resultList, resultCount);
            }
			//xxxxxxxxxx
        }
		//关键核心,将组合后的Delegate组成一个新对象,并填充invocationList,invocationCount
        private MulticastDelegate NewMulticastDelegate(object[] invocationList, int invocationCount, bool thisIsMultiCastAlready)
        {
            // First, allocate a new multicast delegate just like this one, i.e. same type as the this object
            MulticastDelegate result = (MulticastDelegate)InternalAllocLike(this);

            // Performance optimization - if this already points to a true multicast delegate,
            // copy _methodPtr and _methodPtrAux fields rather than calling into the EE to get them
            if (thisIsMultiCastAlready)
            {
                result._methodPtr = this._methodPtr;
                result._methodPtrAux = this._methodPtrAux;
            }
            else
            {
                result._methodPtr = GetMulticastInvoke();
                result._methodPtrAux = GetInvokeMethod();
            }
            result._target = result;
            result._invocationList = invocationList;
            result._invocationCount = invocationCount;

            return result;
        }

多播委托的执行

上面提到,Invoke的本质就是调用_methodPtr所在的函数指针.
那么自然而然,负责执行多播肯定就是_methodPtr了。
从上面的源码可以知道,MulticastDelegate在初始化的时候要调用一次GetMulticastInvoke(),让我们来看看它是什么?
image

哦豁,它还是一个非托管的方法,有兴趣的同学可以自行查看coreclr的c++源码。奥秘就在其中,本人水平有限,怕误人子弟。

简单来说,就是_methodPtr方法在coreclr底层,for循环执行invocationList的委托队列。

思考一个问题,如果只是一个简单的for循环,其中一个委托卡死/执行失败,怎么办?
提示:MulticastDelegate类中有很多override method

非托管委托(函数指针)

C#作为C++的超集,也别名为C++++ 。也可以说是C++的手动挡(JAVA是C++的自动挡)。
自然而然,C++有的,C#也要有。因此在C#11中引入了函数指针,性能更强的同时也继承了C++的所有缺点(除了会在编译期间协助类型安全检查).
https://learn.microsoft.com/zh-cn/dotnet/csharp/language-reference/unsafe-code#function-pointers

泛型委托

为了减轻你工作量,避免创建太多委托定义。BCL提供了Action/Func来提供便利,减少你的代码量。
image

它们在底层与delegate并无区别
image

Lambd表达式

泛型委托是Lambd的基石,其底层还是委托那套东西

            var list = new List<string>();
            var w= list.Where(x => x.Length > 100);

image

事件与委托的关系

CLR事件模型以委托为基础,它们之间的关系。像是对委托的进一步封装。

  1. 事件就是一个语法糖,自己自身并没有新概念
  2. 委托和事件的关系”等同于“字段和属性的关系”

image
事件作为语法糖,IL会在底层生成一个
委托
并提供add_xxxx与remove_xxxx方法对委托进行封装。
image
实际上在底层,还是操作Delegate.Combine那一套东西

背景

.NET 9刚刚正式发布了,如果你创建一个空的Asp.Net Core 9.0的Web API项目,启动之后,你会惊讶地发现陪伴你多年的Swagger没有了!——这是因为ASP.NET Core项目组已经将Swashbuckle.AspNetCore从.NET 9里移除了,详情看这里
[github]Announcement: Swashbuckle.AspNetCore is being removed in .NET 9

image

Swagger被移除的原因可以总结为以下几点:

  • Swashbuckle 维护不力:Swashbuckle 项目不再由社区所有者积极维护,存在许多问题未得到解决,并且未发布兼容 .NET 8 的正式版本。

  • 转向 Microsoft.AspNetCore.OpenApi:ASP.NET Core 团队将增强 Microsoft.AspNetCore.OpenApi 的功能,以取代 Swashbuckle 并实现 OpenAPI 文档生成。

  • 已有替代方案:除了 Swashbuckle,还有 NSwag 等其他项目支持 OpenAPI 文档生成和客户端/服务器代码生成,开发者可以根据项目需求选择合适的方案。

  • 增强内置 API 支持:从 ASP.NET Core 3.1 开始,框架已经提供了 ApiExplorer 等元数据支持,结合 Visual Studio 和 Visual Studio Code 对 .http 文件的内置支持,API 测试和调试体验更佳。

  • 推动 OpenAPI 成为核心组件:ASP.NET Core 团队计划在 .NET 9 中进一步提升 OpenAPI 的集成度,将其作为核心组件,专注于生成 JSON 格式的 OpenAPI 文档。

除了上面提到了NSwag,Scalar也是Swagger优秀的替代品。

Scalar

Scalar 是一个开源的 API 平台, 提供现代化的 REST API 客户端、精美的 API 文档和一流的OpenAPI/Swagger支持,官方几乎支持所有编程语言和平台。
image

Github地址:
https://github.com/scalar/scalar

.NET 9集成Scalar

.NET也是Scalar支持的一等公民,集成非常简单,nuget安装Scalar.AspNetCore包

dotnet add package Scalar.AspNetCore

然后只用增加一个代码即可
app.MapScalarApiReference()

using Scalar.AspNetCore;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddOpenApi();

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.MapScalarApiReference(); // scalar/v1
    app.MapOpenApi();
}

app.MapGet("/", () => "Hello world!");

app.Run();

最后启动项目,打卡
scalar/v1
这个地址就是Scalar界面。
image

界面非常清爽,使用也很简单,并且支持
夜间模式

添加JWT认证

在Scalar添加JWT也很简单,自定义一个
BearerSecuritySchemeTransformer
类来实现
IOpenApiDocumentTransformer
接口即可。

public sealed class BearerSecuritySchemeTransformer(IAuthenticationSchemeProvider authenticationSchemeProvider): IOpenApiDocumentTransformer {
  public async Task TransformAsync(OpenApiDocument document, OpenApiDocumentTransformerContext context, CancellationToken cancellationToken) {
    var authenticationSchemes = await authenticationSchemeProvider.GetAllSchemesAsync();
    if (authenticationSchemes.Any(authScheme => authScheme.Name == "Bearer")) {
      // Add the security scheme at the document level
      var requirements = new Dictionary < string,
        OpenApiSecurityScheme > {
          ["Bearer"] = new OpenApiSecurityScheme {
            Type = SecuritySchemeType.Http,
              Scheme = "bearer", // "bearer" refers to the header name here
              In = ParameterLocation.Header,
              BearerFormat = "Json Web Token"
          }
        };
      document.Components ??= new OpenApiComponents();
      document.Components.SecuritySchemes = requirements;

      // Apply it as a requirement for all operations
      foreach(var operation in document.Paths.Values.SelectMany(path => path.Operations)) {
        operation.Value.Security.Add(new OpenApiSecurityRequirement {
          [new OpenApiSecurityScheme {
            Reference = new OpenApiReference {
              Id = "Bearer", Type = ReferenceType.SecurityScheme
            }
          }] = Array.Empty < string > ()
        });
      }
    }
  }
}

然后注册即可

builder.Services.AddOpenApi(opt =>
{
    opt.UseTransformer<BearerSecuritySchemeTransformer>();
});

最后

Scalar是一个优秀的Swagger替代品,某些功能甚至比Swagger更强大,推荐大家赶紧去试试。

(完)

前言

.NET 9 正式发布,这是迄今为止最高效、现代、安全、智能且高性能的 .NET 版本。

新版本凝聚了全球数千名开发者的共同努力,包含了数千项性能、安全性和功能性改进。

主要亮点

  • 性能提升:全面提升性能,降低内存使用。
  • 安全性增强:显著的安全性改进。
  • AI 支持:轻松将 AI 集成到应用程序中。
  • 全面改进:涵盖编程语言、开发工具和工作负载,提供统一的开发平台。

.NET Conf 2024  大会

.NET Conf 2024 免费参加我们的三天虚拟开发者大会(11 月 12-14 日),深入了解 .NET 9 的新功能。

地址:
https://www.dotnetconf.net/

让我们一起看看 .NET 9 中新增的功能和工具的亮点。

.NET 9 运行时性能改进、更低的内存占用

.NET 9 是迄今为止性能最高的 .NET 版本,进行了 1,000 多项与性能相关的改进,并采用了更高效的算法来生成更好的代码。

Stephen Toub 深入探讨了这些改进,值得阅读。

  • 服务器 GC 优化

服务器 GC 现在根据应用程序的实际内存需求进行调整,而不是依赖于环境中的资源(内存和 CPU)。这一变化特别适用于高核心数环境,尤其是在应用程序内存需求较小或随时间变化较大的情况下。

这种新的 GC 实现减少了内存使用量,但可能会带来轻微的吞吐量成本。用户可以选择配置服务器 GC 使用旧版实现,以进行测试。

  • 性能提升

TechEmpower 基准测试显示,.NET 9 在吞吐量上显著提高,同时内存使用量大幅下降,这主要得益于服务器 GC 的优化。

  • 矢量化和硬件支持

.NET 9 回归矢量化,增加了对 Arm64 SVE、Intel AVX10 等新芯片的支持,以及硬件加速运行时。

  • RyuJIT 性能提升

RyuJIT 提高了 Arm64、循环、PGO 和边界检查的性能。异常处理速度提高了 50%。

  • 动态 PGO 更新

动态配置文件引导优化 (PGO) 更新,优化更多代码模式。JIT 为常见类型转换生成快速路径代码,使执行速度提高 70%,但需要禁用 ReadyToRun。

  • LINQ 优化

LINQ 方法如
Take

DefaultIfEmpty
在空集合时返回速度提高了 10 倍。
Enumerable.SequenceEqual
通过
MemoryExtensions.SequenceEquals
实现高效的迭代和矢量化,现在支持
List<T>

  • System.Text.Json 优化

System.Text.Json
的各种操作性能提升了超过 50%。
JsonProperty.WriteTo
直接写入 UTF8 字节,减少字符串分配。

新 API
JsonMarshal.GetRawUtf8Value
替代
JsonElement.GetRawText
,避免额外的编码和分配。

JsonObject
优化了内存分配和调整大小的成本。

.NET Aspire – 开发更好应用程序的基础组件

.NET Aspire 是一组强大的工具、模板和包,用于无缝开发可观察、生产就绪的应用程序。

自首次发布以来的六个月内,我们对堆栈的所有部分进行了改进,包括遥测和指标仪表板的新功能以及更简化的云应用程序部署。

新功能

从仪表板启动和停止资源。

在调试会话之间保持容器处于活动状态。

新的 API,如 WaitFor,更好地管理资源启动。

新集成

与 OpenAI、Ollama、Milvus 等无缝集成。

简化获取

更轻松地将 .NET Aspire 集成到应用程序中。

改进了 Azure 容器应用程序的部署方案。

预览支持 Azure Functions。

广泛采用

微软内部:Xbox 和 Copilot 团队已将 .NET Aspire 集成到他们现有的服务中,通过易于访问的见解和 Azure 集成收紧了内部开发循环。

社区:.NET Aspire 被广泛采用,社区积极参与并贡献了新的工具和集成。

人工智能——不断发展和进步的 AI 生态系统

.NET 功能不断扩展 ,以帮助我们注入人工智能,主要进展包括:

学习材料和示例

提供新的学习材料和示例,简化与生态系统集成的过程。

合作伙伴和社区

与 Azure、OpenAI、LlamaIndex、Qdrant、Pinecone、Milvus、AutoGen、OllamaSharp、ONNX Runtime 等合作伙伴合作,构建强大的 AI 生态系统。

与社区和控件供应商合作,构建智能组件生态系统,使 AI 集成更加容易。

行业采用

H&R Block、Blip 和 KPMG 等公司已采用 .NET 构建顶级 AI 体验。

.NET 支持 GitHub Copilot 和全新的 Microsoft Copilot 体验。

无缝部署

将 AI 解决方案部署到云端变得更加无缝。

.NET 9 中的 AI 生态系统扩展

强大的生态系统意味着开发人员有更多选择来决定最适合他们的场景。为了简化这些集成并降低进入障碍,我们与 Semantic Kernel 合作,在
Microsoft.Extensions.AI

Microsoft.Extensions.VectorData
下引入了一组抽象,为与 AI 服务交互提供统一的 C# 抽象层。这些抽象支持小型和大型语言模型(SLM 和 LLM)、嵌入、向量存储和中间件。早期采用者如 Pieces 和 OllamaSharp 已经受益于这种新的简化方法。

张量和分词器

在 .NET 9 中,我们对库和基元类型进行了重大改进,以改进 AI 开发:

张量:
引入了新的
Tensor<T>
类型,用于表示多维数据,简化了库和应用操作之间的互操作。

分词器:
Microsoft.ML.Tokenizers
支持多种标记化算法,包括字节级 BPE、SentencePiece 和 WordPiece。

改进了对流行模型系列(如 GPT(4、o1 等)、Llama、Phi 和 Bert)的标记器支持。

降低进入门槛

随着语言模型即服务的兴起,开发人员使用人工智能的进入门槛大大降低。我们提供的这些新构建块旨在帮助开发人员更轻松地集成和使用 AI 服务。

快速开始

如感兴趣的朋友可以浏览

AI 文档:
https://devblogs.microsoft.com/dotnet/announcing-dotnet-9/aiextensions.svg

.NET 的 AI 示例:
https://devblogs.microsoft.com/dotnet/announcing-dotnet-9/aiextensions.svg

针对 .NET 开发人员的 GitHub Copilot 增强功能

GitHub Copilot 通过改善编辑器体验并在常规工作流程中为 .NET 开发人员提供 AI 帮助,显著提高了工作效率。

最新版本的 Visual Studio 和 Visual Studio Code 以及 GitHub Copilot 的更新,带来了以下新功能:

  • AI 智能变量检查
  • 通过集成 AI 变量检查优化调试工作流程。
  • AI 驱动的 IEnumerable 可视化工具
  • 在 IEnumerable 可视化工具中,提供 AI 驱动的 LINQ 可编辑表达式。
  • 使用 GitHub Copilot 修复代码
  • GitHub Copilot 帮助解决代码问题。
  • 更好的 C# 人工智能补全
  • GitHub Copilot 从相关源文件中引入额外的上下文,改进 C# 补全。
  • 使用 GitHub Copilot 调试测试
  • 通过 GitHub Copilot 的调试测试功能,获取调试失败测试的帮助。

ASP.NET Core 和 Blazor 进行全栈 Web 开发

ASP.NET Core 是 .NET 全栈 Web 框架,提供现代 Web 应用程序和可扩展后端服务所需的一切。

.NET 9 带来了新功能以及显著的性能、可访问性和安全性改进。

优化静态文件处理

在构建和发布期间优化静态 Web 资源(如 JavaScript 和 CSS 文件),通过向文件名添加基于内容的哈希生成指纹版本,确保文件名唯一并避免使用过时版本。

发布时使用 Brotli 预压缩文件,减少下载大小并减轻服务器负担。

使用端点路由处理静态文件,支持每端点授权等高级功能。

Blazor 改进
性能提升

  • 性能在所有领域都有所改进。

  • 新的 Blazor 混合和 Web 应用模板。

  • 新 API 使开发人员能够创建更流畅的用户体验。

组件渲染模式检测

  • 使用
    RendererInfo
    API 在运行时检测组件渲染模式,预渲染期间禁用或隐藏交互元素,组件交互后启用它们。

重新连接体验

  • Blazor Server 应用程序受益于更友好的 UI、更快的重新连接和自动处理连接丢失时的页面重新加载。

OpenAPI 增强功能

内置 OpenAPI 支持

  • 使用
    Microsoft.AspNetCore.OpenAPI
    包自动生成 OpenAPI 文档,元数据从应用程序代码、属性和扩展方法中提取。

  • 支持对操作、模式或整个文档进行定制。

  • Minimal API 应用程序中支持原生 AOT,优化性能。

  • 构建时生成 OpenAPI 文档,集成到本地开发工作流程中。

安全性改进
开发证书设置

  • 在 Linux 上更轻松地设置受信任的开发证书,启用 HTTPS。

身份验证状态传输

  • Blazor 提供内置 API 用于将身份验证状态传输到客户端。

OAuth 和 OIDC 支持

  • 支持向 OAuth 和 OIDC 授权请求添加额外参数,以及对推送授权请求 (PAR) 的支持。

数据保护和连接指标

  • 强化数据保护支持。

  • 改进 Kestrel 的连接指标,便于检测连接失败原因。

.NET MAUI – 增强多平台应用程序开发

.NET MAUI 是使用 .NET 跨移动和桌面构建多平台应用程序的终极工具。

.NET 9 的主要目标是提高质量和可靠性,使您能够更轻松地在生产中交付应用程序。

社区参与和贡献

  • 使用 .NET MAUI 构建的应用程序在 Google Play 商店中的数量翻倍。

  • 开发人员使用率增长了 30% 以上,达到历史最高水平。

  • 社区参与度和贡献显著增加。

  • Syncfusion 自今年 7 月到 9 月开始贡献,占所有社区贡献的 55%,比前 3 个月增长了 557%。

新项目模板

  • 引入全新的项目模板,包含 14 个免费开源 Syncfusion 控件和其他社区流行库。

  • 演示 MVVM、数据库访问、导航、视图刷新等推荐实践,帮助快速启动应用程序开发。

性能和可靠性增强

  • 全面改进 .NET MAUI 的性能,包括 iOS 和 Mac Catalyst 的新实现
    CollectionView

    CarouselView

  • 更新现有控件和应用程序生命周期,支持本机 AOT 和修剪增强功能,构建更小、更快的应用程序。

平台支持

  • 支持最新的 iOS、macOS 和 Android 操作系统。

  • 添加了多种本机平台功能,如 Android Asset Pack、改进的与本机库的互操作性,以及通过新 Xcode dotnet 工具简化 Xcode 和 Visual Studio Code 之间的集成。

使用 .NET 9 进行 Windows 开发

借助 .NET 9 的 Windows 应用程序将能够访问最新的操作系统特性和功能,同时确保更高的性能和可访问性。无论是使用 WinUI 3 和 Windows App SDK 构建新的现代应用程序,还是对现有的 WPF 和 WinForms 应用程序进行现代化改造,.NET 9 都能提供最佳支持。

WinUI 3

本机 AOT 支持,实现更小、性能更高的应用程序。

WPF

Fluent UI 现代主题增强,提升用户界面体验。

WinForms

新增深色模式、现代图标 API 和改进的异步 API 访问(如 Control.InvokeAsync)。

C# 13 和 F# 9 新功能

C#
是世界上最受欢迎和喜爱的编程语言之一。

C# 13 专注于让我们能够更轻松、更安全、更快速地编写代码。

集合表达式

params修饰符现在支持任何受支持的集合类型,而不仅仅是数组。

高性能代码

通过使用 ref struct 和 System.Threading.Lock,解锁更高性能的代码和更轻松的多线程应用。

Lock myLock = new();
void Concat<T>(params List<T>items)
{
lock(myLock)
Console.WriteLine(
string.Join("\e[1mItem: \e[0m", items));
}

F# 9 继续为 .NET 开发人员提供最佳的函数式编程体验。

主要改进包括:

可空引用类型
:与 C# 库的交互更加类型安全。

优化的整数范围
:加快循环和其他推导式的速度。

优化的相等检查
:避免装箱,提高常见操作的性能。

自动生成的
.Is*
属性

:为受歧视的联合提供快速案例测试。

标准库增强
:包含集合的随机函数,适用于数据科学和游戏开发。

改进的诊断和工具
:提高开发人员的生产力。

//FS3261: Nullness warning: The types 'string' and 'string | null' do not have equivalent nullability.
let methodArgument (s: string | null) =File.Create s
let matchNullableString(s:
string | null) =match s with| null -> 0 | notNull -> notNull.Length //notNull is of type string

.NET 9 开发工具更新

.NET 9 附带了多项开发工具更新,旨在提高开发人员的工作效率。

Visual Studio 2022 17.12

  • 性能提升
    :显著提升性能,提高开发效率。

  • 调试和诊断
    :增强的调试和诊断体验。

  • .NET Aspire 集成
    :与 .NET Aspire 更紧密的集成。

  • 云集成
    :更深入的云集成。

  • C# 13 支持
    :支持 C# 13 的新功能。

  • Git 支持
    :增强的 Git 支持。

  • 用户请求功能
    :包含大量用户请求的功能。

Visual Studio Code 的 C# 开发工具包

  • 编辑可靠性
    :改进编辑可靠性。

  • NuGet 包管理
    :增强的 NuGet 包管理。

  • 测试适配器
    :增强的测试适配器和代码覆盖率结果。

  • .NET MAUI 支持
    :改进的 .NET MAUI 开发支持。

  • 项目启动/调试
    :升级的项目启动和调试配置。

.NET CLI 改进

  • 终端记录器
    :全面改进终端记录器,包括可点击链接、持续计时器和颜色编码。

  • 简洁输出
    :记录器输出更加简洁,提供失败和警告总数的摘要。

  • 软件包审计

    dotnet restore
    更新,审核顶级依赖项和传递依赖项的软件包漏洞。

  • 中央包管理
    :与中央包管理完美配合,快速将包升级部署到所有项目。

  • 漏洞警报
    :在终端、Visual Studio 和 CI/CD 管道中提供包漏洞警报,由 GitHub Advisory Database 驱动。

立即体验 .NET 9 相关内容

.NET 9 以及 Visual Studio 2022 和 Visual Studio Code 的 C# 开发工具包的更新现已发布。

下载链接

了解新增功能

最后

如果你觉得这篇文章对你有帮助,不妨点个赞支持一下!你的支持是我继续分享知识的动力。如果有任何疑问或需要进一步的帮助,欢迎随时留言。

也可以加入微信公众号
[DotNet技术匠]
社区,与其他热爱技术的同行一起交流心得,共同成长!
优秀是一种习惯,欢迎大家留言学习!

译文:
https://devblogs.microsoft.com/dotnet/announcing-dotnet-9/

大家好,我是 V 哥。今天看了阿里云开发者社区关于 Java 的灵魂拷问,一线大厂在用 Java 时,都会考虑哪些问题呢,对于工作多年,又没有大厂经历的小伙伴不妨看看,V 哥总结的这13个为什么,你都会哪些?先赞后看,绝不摆烂。

1. 为什么禁止使用 BigDecimal 的 equals 方法做等值比较?

BigDecimal

equals
方法在等值比较时存在一些问题,通常不建议直接使用它来判断数值的相等性。下面是主要原因以及推荐的替代方案:

1.
equals
方法比较严格,包含了精度和符号的比较

BigDecimal.equals
不仅比较数值本身,还会比较精度和符号。例如,
BigDecimal

equals
方法会认为
1.0

1.00
是不同的值,因为它们的
scale
不同(即小数位数不同)。例如:

BigDecimal a = new BigDecimal("1.0");
BigDecimal b = new BigDecimal("1.00");

System.out.println(a.equals(b)); // 输出 false

尽管
1.0

1.00
数值上是相等的,但
equals
方法会因为精度不同返回
false

2.
equals
方法会区分正负零


BigDecimal
中,正零 (
0.0
) 和负零 (
-0.0
) 是不相等的,而使用
equals
会导致
0.0

-0.0
被视为不相等。例如:

BigDecimal zero1 = new BigDecimal("0.0");
BigDecimal zero2 = new BigDecimal("-0.0");

System.out.println(zero1.equals(zero2)); // 输出 false

这可能会导致误判,因为在大多数业务逻辑中,我们认为
0.0

-0.0
是等值的。

推荐的替代方案:使用
compareTo
方法

为了避免这些问题,建议使用
BigDecimal.compareTo
方法。
compareTo
方法仅比较数值的大小,不关注精度和符号。因此,在需要判断两个
BigDecimal
是否等值时,使用
compareTo
更为合理:

BigDecimal a = new BigDecimal("1.0");
BigDecimal b = new BigDecimal("1.00");

System.out.println(a.compareTo(b) == 0); // 输出 true

在这种情况下,
1.0

1.00
被视为相等,即使它们的精度不同,
compareTo
也会返回
0

小结

  • 不要使用
    equals
    方法

    :它会考虑精度和符号,容易导致误判。
  • 推荐使用
    compareTo
    方法

    :只比较数值,忽略精度和正负零的差异,可以实现更符合业务需求的等值比较。

2. 为什么禁止使用double直接构造BigDecimal?

在使用
BigDecimal
时,不建议直接使用
double
作为构造参数。这是因为
double
类型在 Java 中的表示是基于二进制浮点数的,会引入精度误差,从而导致不准确的结果。例如:

double d = 0.1;
BigDecimal bd = new BigDecimal(d);
System.out.println(bd); // 输出 0.1000000000000000055511151231257827021181583404541015625

原因解析

  1. 二进制浮点数的精度问题
    double
    使用 IEEE 754 标准表示小数,在二进制系统中,像
    0.1
    这样的小数无法精确表示,导致它在存储时会变成一个近似值。这个近似值会直接传递给
    BigDecimal
    的构造方法,从而生成带有误差的
    BigDecimal
    值。

  2. 结果不准确,影响业务计算
    在一些金融计算或其他对精度要求高的场景中,直接使用
    double
    构造
    BigDecimal
    会带来潜在的误差积累,从而影响最终的结果。例如,在多次计算或累加时,误差可能不断放大。

推荐的替代方案

  • 使用字符串或精确值构造
    BigDecimal

    通过传入字符串形式的数字,可以避免精度误差,因为字符串构造器不会引入任何二进制的近似计算。
  BigDecimal bd = new BigDecimal("0.1");
  System.out.println(bd); // 输出 0.1
  • 使用
    BigDecimal.valueOf(double)
    方法

    另一个安全的方式是使用
    BigDecimal.valueOf(double)
    ,该方法会将
    double
    转换为
    String
    表示,然后构造
    BigDecimal
    ,从而避免精度损失。
  BigDecimal bd = BigDecimal.valueOf(0.1);
  System.out.println(bd); // 输出 0.1

小结

  • 避免直接使用
    double
    构造
    BigDecimal

    ,以免引入二进制浮点数的精度误差。
  • 优先使用字符串构造器
    ,或使用
    BigDecimal.valueOf(double)
    以确保精度。

3. 为什么禁止使用 Apache Beanutils 进行属性的 copy ?

Apache BeanUtils
是一个早期用于 Java Bean 属性复制的工具库,但在现代 Java 开发中通常不推荐使用它来进行属性的拷贝,尤其在性能敏感的场景中。原因主要包括以下几点:

1.
性能问题

Apache BeanUtils.copyProperties()
使用了大量的反射操作,且每次拷贝都需要对字段、方法进行查找和反射调用。反射机制虽然灵活,但性能较低,尤其是在大量对象或频繁拷贝的场景中,会产生显著的性能瓶颈。

相比之下,
Spring BeanUtils

Apache Commons Lang

FieldUtils
等工具经过优化,使用了更高效的方式进行属性复制。在性能要求较高的场合,
MapStruct

Dozer
等编译期代码生成的方式则可以完全避免运行时反射。

2.
类型转换问题

BeanUtils.copyProperties
在属性类型不匹配时会隐式地进行类型转换。例如,将
String
类型的
"123"
转换为
Integer
,如果转换失败,会抛出异常。这种隐式转换在处理数据时,可能带来不易察觉的错误,而且并不总是适合应用场景。

在精确的属性复制需求下,通常希望类型不匹配时直接跳过拷贝,或明确抛出错误,而不是隐式转换。例如,
Spring BeanUtils.copyProperties
不会进行隐式转换,适合严格的属性匹配场景。

3.
潜在的安全问题

Apache BeanUtils

PropertyUtils
组件在执行反射操作时存在一定的安全隐患。历史上,
BeanUtils

PropertyUtils
曾有安全漏洞,使恶意用户可以通过精心构造的输入利用反射机制执行系统命令或加载恶意类。尽管这些漏洞在现代版本中已得到修复,但该库的架构和实现仍较为陈旧,难以应对更高的安全需求。

4.
缺乏对嵌套对象的深拷贝支持

BeanUtils.copyProperties
仅支持浅拷贝,即只能复制对象的一级属性,无法递归地对嵌套对象进行复制。如果对象包含了复杂的嵌套结构,使用
BeanUtils.copyProperties
很容易出现意外行为或数据丢失。像
MapStruct

Dozer
这样的工具则提供对嵌套对象的深层复制能力,更适合复杂对象的深度拷贝需求。

推荐的替代方案

  1. Spring
    BeanUtils.copyProperties()

    Spring 的
    BeanUtils.copyProperties()
    提供了更优的性能和更好的类型安全性。它不做类型转换,且提供了方便的过滤器用于选择性拷贝属性。

  2. MapStruct
    MapStruct
    是基于注解的对象映射框架,支持编译期生成代码,完全避免了反射的性能开销,且支持复杂对象、嵌套属性的深度拷贝,是性能要求较高的首选。

  3. Dozer
    Dozer
    支持更灵活的映射配置和深拷贝,适合对象结构复杂的情况。它可以处理嵌套属性映射、类型转换,且具有较好的自定义能力。

小结

Apache BeanUtils.copyProperties
不适合现代 Java 开发的性能、安全性和灵活性要求,推荐使用更高效、安全、灵活的框架(如 Spring
BeanUtils

MapStruct
等)来代替。

4. 为什么要求日期格式化时必须有使用 y 表示年,而不能用 Y ?

在日期格式化中,必须使用
y
而不是
Y
来表示年份,这是因为
y

Y
在 Java 和其他日期格式化工具中代表不同的含义:

  1. y
    表示日历年(Calendar Year)

    y
    是标准的表示年份的字符,表示的是通常意义上的公历年,比如
    2024
    表示的就是这一年的年份。使用
    y
    时,日期格式化工具会准确地格式化出对应的年份数值:
   SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
   System.out.println(sdf.format(new Date())); // 输出: 2024-11-10
  1. Y
    表示星期年(Week Year)

    Y
    表示的是“星期年”或称“ISO周年”(ISO week-numbering year),它是一种基于ISO周数的年份表示方式。这种表示法根据每年的第一个星期一所在的周来计算年份,如果某天属于新一年的第一个完整星期,则会归为新年的星期年。

    例如,如果某年的最后几天在下一年开始的第一个星期中,它们可能会被归入下一年的
    week year
    。同理,如果新年的前几天在上一年的最后一个完整星期内,这些天的星期年可能会归属上一年。这在日期和时间处理中可能导致意外的年份差异。

   SimpleDateFormat sdf = new SimpleDateFormat("YYYY-MM-dd");
   System.out.println(sdf.format(new Date())); // 可能输出与实际年份不同的值

使用
Y
的潜在问题

使用
Y
表示年份会引发一些日期计算的错误,因为它依赖于周数的计算方式,不是每次都与实际的公历年份一致。例如:

  • 2024年12月31日会被视作
    2025

    week year
    ,导致使用
    YYYY
    格式化时得到
    2025-12-31
  • 在跨年计算或特定日期逻辑中使用
    Y
    表示年份可能会出现错误,因为
    week year
    与通常理解的日历年并不总是相符。

什么时候使用
Y

Y
一般仅用于需要符合 ISO 8601 标准的日期格式,特别是包含 ISO 周数(如“2024-W01-1”表示2024年的第一个星期一)的情况,而在一般情况下,我们都应使用
y
来表示日历年份。

小结

  • 使用
    y
    来表示常规年份

    ,避免日期格式化错误。
  • 避免使用
    Y
    来表示年份

    ,除非确实需要按照 ISO 周年的格式来解析和显示年份。

5. 为什么使用三目运算符时必需要注意类型对齐?

在使用三目运算符时,类型对齐非常重要,因为三目运算符的两个分支会被类型推断成一个共同的类型。若两者类型不同,Java 编译器会进行类型提升或自动转换,这可能导致意外的类型变化和潜在的错误。以下是需要注意的原因和细节:

1. 三目运算符会自动进行类型提升

三目运算符的返回值类型是根据
true

false
分支的类型推断出来的。为了得到一致的结果,Java 会自动将不同的类型提升为更高精度的类型。例如,若一个分支返回
int
而另一个分支返回
double
,Java 会将
int
提升为
double

int x = 5;
double y = 10.5;
double result = (x > 0) ? x : y; // 返回 double 类型
System.out.println(result); // 输出 5.0

这里返回值
5
被提升为
5.0
。虽然代码在这个例子中不会出错,但在某些情况下,这种自动提升会导致意外的精度损失或类型不匹配的问题。

2. 自动拆箱和装箱可能引发
NullPointerException

在 Java 中,基本类型和包装类型的对齐需要特别小心。三目运算符会尝试将包装类型和基本类型对齐成相同类型,这会导致自动装箱和拆箱,如果某个分支为
null
且需要拆箱,可能会引发
NullPointerException

Integer a = null;
int b = 10;
int result = (a != null) ? a : b; // 如果 a 为 null,结果会发生自动拆箱,引发 NullPointerException

由于
a

null
,Java 会尝试将其拆箱为
int
,从而抛出
NullPointerException
。为避免这种情况,可以确保类型对齐,或避免对可能为
null
的对象进行拆箱。

3. 返回值类型不一致可能导致编译错误

如果三目运算符的两种返回类型无法被编译器自动转换为一个兼容类型,代码会直接报错。例如:

int x = 5;
String y = "10";
Object result = (x > 0) ? x : y; // 编译错误:int 和 String 不兼容

在这种情况下,
int

String
无法被提升到相同类型,因此会引发编译错误。若确实希望返回不同类型的值,可以手动指定共同的超类型,例如将结果定义为
Object
类型:

Object result = (x > 0) ? Integer.valueOf(x) : y; // 这里 result 为 Object

4. 类型对齐可以提升代码的可读性

保持三目运算符返回的类型一致,能让代码更加清晰,便于理解和维护。类型对齐可以避免类型转换和自动提升带来的混乱,使代码更容易预测和理解:

double result = (condition) ? 1.0 : 0.0; // 返回 double

小结

  • 保持类型一致性
    ,确保
    true

    false
    分支的类型相同,避免意外的类型提升。
  • 小心自动装箱和拆箱
    ,避免
    null
    参与三目运算符计算。
  • 在返回不同类型时选择合适的公共类型
    ,如使用
    Object
    或显式转换。

6. 为什么建议初始化 HashMap 的容量大小?

初始化
HashMap
的容量大小是为了提高性能和减少内存浪费。通过设置合适的初始容量,可以减少
HashMap
的扩容次数,提高程序运行效率。以下是详细原因和建议:

1. 减少扩容次数,提高性能

HashMap
默认的初始容量为 16,当超过负载因子阈值(默认是 0.75,即达到容量的 75%)时,
HashMap
会自动进行扩容操作,将容量扩大为原来的两倍。扩容涉及到重新计算哈希并将数据重新分布到新的桶中,这个过程非常耗时,尤其在元素较多时,扩容会显著影响性能。

通过设置合适的初始容量,可以避免或减少扩容操作,提高
HashMap
的存取效率。

2. 节省内存,避免不必要的内存开销

如果预计要存储大量数据但没有指定容量,
HashMap
可能会多次扩容,每次扩容会分配新的内存空间,并将原有数据复制到新空间中,造成内存浪费。如果在创建
HashMap
时能合理估算其容量,则可以一次性分配足够的空间,从而避免重复分配内存带来的资源浪费。

3. 避免扩容带来的线程安全问题

在并发环境下,频繁扩容可能导致线程不安全,即使是
ConcurrentHashMap
也不能完全避免扩容带来的性能和一致性问题。初始化合适的容量可以减少并发环境下扩容带来的风险。

如何估算合适的容量

  1. 预估数据量
    :如果预计
    HashMap
    将存储
    n
    个元素,可以将初始容量设置为
    (n / 0.75)
    ,再向上取整为最接近的 2 的幂次方。
   int initialCapacity = (int) Math.ceil(n / 0.75);
   Map<String, String> map = new HashMap<>(initialCapacity);
  1. 取 2 的幂次方

    HashMap
    的容量总是以 2 的幂次方增长,因为在进行哈希运算时,可以高效利用按位与操作来计算哈希桶索引。因此,初始容量设为 2 的幂次方会使哈希分布更均匀。

示例代码

int expectedSize = 1000; // 预估需要存储的键值对数量
int initialCapacity = (int) Math.ceil(expectedSize / 0.75);
HashMap<String, Integer> map = new HashMap<>(initialCapacity);

小结

初始化
HashMap
的容量大小有以下好处:

  • 提高性能
    :减少扩容次数,优化存取效率。
  • 节省内存
    :避免多次扩容引起的内存浪费。
  • 提升线程安全
    :在并发环境下减少扩容带来的线程不安全风险。

合理初始化
HashMap
容量对于高性能应用尤为重要,尤其在存储大量数据时可以显著提升程序的运行效率。

7. 为什么禁止使用 Executors 创建线程池?

在 Java 中创建线程池时,不推荐直接使用
Executors
提供的快捷方法(例如
Executors.newFixedThreadPool()

Executors.newCachedThreadPool()
等),而推荐使用
ThreadPoolExecutor
构造方法来手动配置线程池。这种做法主要是为了避免
Executors
创建线程池时隐藏的风险,确保线程池配置符合需求。具体原因如下:

1.
不透明的任务队列长度导致OOM风险

  • newFixedThreadPool()

    newSingleThreadExecutor()

    使用的是
    无界队列
    LinkedBlockingQueue
    。无界队列可以存放无限数量的任务,一旦任务量非常大,队列会迅速占用大量内存,导致
    OutOfMemoryError
    (OOM)。

  • newCachedThreadPool()
    使用的是
    SynchronousQueue
    ,该队列没有存储任务的能力,每个任务到来时必须立即有一个空闲线程来处理任务,否则将创建一个新线程。当任务到达速度超过线程销毁速度时,线程数量会快速增加,导致
    OOM

2.
线程数无法控制,导致资源耗尽


newCachedThreadPool()
创建的线程池中,线程数没有上限,短时间内大量请求会导致线程数暴增,耗尽系统资源。
newFixedThreadPool()

newSingleThreadExecutor()
虽然限制了核心线程数,但未限制任务队列长度,依然可能耗尽内存。

在业务需求不确定或任务激增的场景下,建议明确限制线程池的最大线程数和队列长度,以更好地控制系统资源的使用,避免因线程数无法控制导致的性能问题。

3.
缺乏合理的拒绝策略控制

  • Executors
    创建的线程池默认使用
    AbortPolicy
    拒绝策略,即当线程池达到饱和时会抛出
    RejectedExecutionException
    异常。
  • 不同的业务场景可能需要不同的拒绝策略,例如可以使用
    CallerRunsPolicy
    (让提交任务的线程执行任务)或
    DiscardOldestPolicy
    (丢弃最旧的任务)来平衡任务处理。

手动创建
ThreadPoolExecutor
时,可以指定适合业务需求的拒绝策略,从而更灵活地处理线程池满载的情况,避免异常或系统性能下降。

4.
灵活配置核心参数

使用
ThreadPoolExecutor
的构造方法可以手动设置以下参数,以便根据业务需求灵活配置线程池:

  • corePoolSize
    :核心线程数,避免空闲线程被频繁销毁和重建。
  • maximumPoolSize
    :最大线程数,控制线程池能使用的最大资源。
  • keepAliveTime
    :非核心线程的存活时间,适合控制线程销毁频率。
  • workQueue
    :任务队列类型和长度,便于管理任务积压的情况。

这些参数的合理配置可以有效平衡线程池的性能、资源占用和任务处理能力,避免使用默认配置时不符合需求的情况。

推荐的线程池创建方式

建议直接使用
ThreadPoolExecutor
构造方法配置线程池,例如:

int corePoolSize = 10;
int maximumPoolSize = 20;
long keepAliveTime = 60L;
BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(100);

ThreadPoolExecutor executor = new ThreadPoolExecutor(
    corePoolSize,
    maximumPoolSize,
    keepAliveTime,
    TimeUnit.SECONDS,
    workQueue,
    new ThreadPoolExecutor.CallerRunsPolicy()  // 拒绝策略
);

小结

使用
Executors
创建线程池会带来不易察觉的风险,可能导致系统资源耗尽或任务堆积,手动配置
ThreadPoolExecutor
可以更好地控制线程池的行为,使其符合实际业务需求和资源限制。因此,为了系统的健壮性和可控性,建议避免使用
Executors
快捷方法来创建线程池。

8. 为什么要求谨慎使用 ArrayList 中的 subList 方法?

在使用
ArrayList

subList
方法时需要谨慎,因为它有一些潜在的陷阱,容易导致意外的错误和难以排查的异常。以下是
subList
需要小心使用的原因和注意事项:

1.
subList
返回的是视图,而不是独立副本

ArrayList

subList
方法返回的是原列表的一部分视图(
view
),而不是一个独立的副本。对
subList
的修改会直接影响原列表,反之亦然:

ArrayList<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5));
List<Integer> subList = list.subList(1, 4);
subList.set(0, 10); // 修改 subList
System.out.println(list); // 原列表也受到影响:[1, 10, 3, 4, 5]

这种共享视图的机制在某些场景中可能引发意外的修改,导致数据被意外改变,从而影响到原始数据结构的完整性和正确性。

2.
subList
的结构性修改限制

当对
ArrayList
本身(而非
subList
视图)进行结构性修改(
add

remove
等改变列表大小的操作)后,再操作
subList
会导致
ConcurrentModificationException
异常。这是因为
subList
和原
ArrayList
之间共享结构性修改的状态,一旦其中一个发生修改,另一方就会失效:

ArrayList<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5));
List<Integer> subList = list.subList(1, 4);
list.add(6); // 修改原列表的结构
subList.get(0); // 抛出 ConcurrentModificationException

这种限制意味着
subList
不适合在列表频繁变化的场景中使用,否则很容易引发并发修改异常。

3.
subList

ArrayList
的 removeAll 等操作可能导致错误

subList
生成的视图列表可能会在批量删除操作中出现问题,例如调用
removeAll
方法时,
subList
的行为不一致或发生异常。对于
ArrayList

subList
,一些批量修改方法(如
removeAll

retainAll
)可能会在删除视图元素后,导致
ArrayList
产生不可预料的状态,甚至引发
IndexOutOfBoundsException
等异常。

4. 推荐的安全使用方式

如果需要一个独立的子列表,可以通过
new ArrayList<>(originalList.subList(start, end))
来创建一个子列表的副本,从而避免
subList
的共享视图问题:

ArrayList<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5));
ArrayList<Integer> subListCopy = new ArrayList<>(list.subList(1, 4)); // 创建副本
list.add(6); // 修改原列表
subListCopy.get(0); // 安全,不会受到影响

小结

使用
ArrayList

subList
方法需要注意以下几点:

  • 视图机制

    subList
    只是原列表的视图,修改其中一个会影响另一个。
  • 结构性修改限制
    :结构性修改原列表后再访问
    subList
    会抛出
    ConcurrentModificationException
  • 批量操作问题

    subList
    的批量操作可能引发不可预料的错误。
  • 建议创建副本
    :如需独立操作子列表,最好创建
    subList
    的副本以避免潜在问题。

谨慎使用
subList
可以避免意外的错误,提高代码的健壮性。

9. 为什么禁止在 foreach 循环里进行元素的 remove/add 操作?

在 Java 中,禁止在
foreach
循环中进行元素的
remove

add
操作,主要是因为这种操作可能导致
ConcurrentModificationException
异常,或者导致循环行为不符合预期。具体原因如下:

1.
ConcurrentModificationException
异常

当你在
foreach
循环中直接修改集合(例如
remove

add
元素),会导致并发修改问题。
foreach
循环底层使用了集合的
Iterator
来遍历元素。大多数集合类(如
ArrayList

HashSet
等)都会维护一个
modCount
计数器,表示集合的结构变更次数。当你在遍历时修改集合的结构(如删除或添加元素),
modCount
会发生变化,而
Iterator
会检测到这种结构性修改,从而抛出
ConcurrentModificationException
异常,防止程序在多线程环境中出现意外行为。

例如:

List<String> list = new ArrayList<>(Arrays.asList("a", "b", "c", "d"));
for (String s : list) {
    if (s.equals("b")) {
        list.remove(s);  // 会抛出 ConcurrentModificationException
    }
}

在上面的代码中,
foreach
循环遍历
list
时,如果删除了元素
b
,它会修改
list
的结构,从而导致
Iterator
检测到并发修改,抛出异常。

2.
不可预测的行为

即使没有抛出
ConcurrentModificationException
,在
foreach
循环中修改集合也会导致不可预测的行为。例如,
remove

add
操作会改变集合的大小和内容,可能会影响迭代的顺序或导致遗漏某些元素,甚至造成死循环或跳过某些元素。

例如:

List<String> list = new ArrayList<>(Arrays.asList("a", "b", "c", "d"));
for (String s : list) {
    if (s.equals("b")) {
        list.add("e");  // 修改集合的大小
    }
    System.out.println(s);
}

在这个例子中,
add
操作会向
list
中添加一个新元素
"e"
,从而修改了集合的结构。因为
foreach
循环的内部实现使用了迭代器,它可能不会考虑到修改后的新元素,导致输出顺序或遍历结果与预期不同。

3.
迭代器的
remove()
方法

如果需要在循环中删除元素,推荐使用
Iterator
显式地进行删除操作。
Iterator
提供了一个安全的
remove()
方法,可以在遍历时安全地删除元素,而不会引发
ConcurrentModificationException

例如:

List<String> list = new ArrayList<>(Arrays.asList("a", "b", "c", "d"));
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
    String s = iterator.next();
    if (s.equals("b")) {
        iterator.remove();  // 使用 Iterator 的 remove() 方法
    }
}

使用
Iterator.remove()
可以安全地在遍历时删除元素,而不会抛出并发修改异常。

小结


foreach
循环中直接进行
remove

add
操作是不安全的,主要有以下原因:

  • ConcurrentModificationException
    :直接修改集合会触发迭代器的并发修改检测,导致异常。
  • 不可预测的行为
    :修改集合的结构可能导致元素遗漏、顺序错乱或程序逻辑出错。
  • 使用
    Iterator
    替代

    :使用
    Iterator

    remove()
    方法可以避免这些问题,实现安全的元素删除操作。

因此,正确的做法是使用
Iterator
显式地处理元素的删除或修改,而不是直接在
foreach
循环中进行修改。

10. 为什么禁止工程师直接使用日志系统 (Log4j、Logback) 中的 API ?

在很多工程实践中,
禁止工程师直接使用日志系统(如 Log4j、Logback)中的 API
,主要是出于以下几个原因:

1.
日志配置与实现的分离

直接使用日志系统的 API 可能会导致日志记录逻辑与应用的业务逻辑紧密耦合,使得日志配置和实现的分离变得困难。现代的日志框架(如 Log4j、Logback)允许通过外部配置文件(如
log4j.xml

logback.xml
)灵活配置日志级别、输出格式、输出位置等,而不是硬编码到应用代码中。直接使用日志 API 会导致日志的配置与业务代码绑定在一起,不易修改和维护。

建议的做法
:通过使用日志框架的日志抽象接口(如
org.slf4j.Logger
)来记录日志,而不是直接依赖具体的日志实现。这种方式提供了更大的灵活性,日志实现可以在运行时通过配置文件更换而无需修改代码。

2.
灵活性与可扩展性问题

如果工程师直接使用日志库的 API,项目在需要切换日志框架(比如从 Log4j 转换到 Logback 或其他框架)时,需要修改大量的代码,增加了系统的耦合度和维护难度。另一方面,使用日志抽象层(如 SLF4J)可以避免这一问题,因为 SLF4J 是一个日志抽象层,底层可以切换具体的日志实现而无需改变业务代码。

示例

// 不推荐:直接使用 Log4j 的 API
import org.apache.log4j.Logger;
Logger logger = Logger.getLogger(MyClass.class);
logger.info("This is a log message");

// 推荐:通过 SLF4J 接口来记录日志
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Logger logger = LoggerFactory.getLogger(MyClass.class);
logger.info("This is a log message");

使用 SLF4J 可以在不同的环境中灵活切换日志实现,而无需修改代码。

3.
日志记录与调试不一致

如果工程师直接使用日志框架的 API,可能会在日志记录时不遵循一致的日志策略。例如,日志的级别、格式、日志输出的内容等可能不统一,导致日志信息混乱、不易追踪。通过统一的日志抽象接口(如 SLF4J)和规范的日志记录策略(通过 AOP 或日志框架自带的特性)可以保持日志的一致性和规范性。

最佳实践

  • 通过统一的日志管理类或工具类来封装日志记录方法,确保所有日志记录都遵循统一的格式和规范。
  • 在日志中统一使用适当的日志级别(如
    DEBUG

    INFO

    WARN

    ERROR
    )和标准格式。

4.
日志的性能影响

日志记录可能对应用的性能产生一定的影响,尤其是在日志记录过于频繁或日志输出内容过多的情况下。通过直接使用日志框架的 API,可能无法灵活控制日志输出的频率、内容或过滤策略,从而造成性能问题。很多日志框架(如 Log4j 和 Logback)提供了高级的配置选项,如异步日志、日志缓存等特性,可以显著提高性能。

推荐做法

  • 使用日志框架提供的异步日志功能来提高性能。
  • 配置适当的日志级别,避免在生产环境中输出过多的调试信息。

5.
日志管理的统一性与规范

在团队开发中,直接使用日志框架的 API 会导致不同开发人员在不同模块中记录日志时不遵循统一规范,导致日志格式不统一、信息不一致,甚至产生重复的日志记录。通过日志管理工具类或封装类,可以确保所有开发人员遵循统一的日志记录策略。

示例

  • 创建一个统一的
    LoggerFactory
    工厂类来生成日志记录对象。
  • 统一定义日志级别和输出格式,确保日志输出一致。

小结

禁止工程师直接使用日志系统(如 Log4j、Logback)中的 API,主要是为了:

  1. 解耦日志实现与业务逻辑
    :通过使用日志抽象层(如 SLF4J),可以更轻松地切换日志框架,避免硬编码。
  2. 提高灵活性与可维护性
    :避免在应用中重复使用框架 API,提高日志配置的灵活性和一致性。
  3. 规范日志记录行为
    :通过封装日志记录,确保日志级别、格式和内容的统一,增强可读性和可追踪性。
  4. 优化性能
    :通过配置日志框架的高级功能(如异步日志),提高日志系统的性能,减少对应用的影响。
  5. 统一日志管理
    :避免团队成员在不同模块中使用不一致的日志记录方式,确保日志输出的标准化。

最好的做法是通过日志抽象层(如 SLF4J)进行日志记录,同时通过日志管理工具类进行统一的配置和调用,确保日志的高效、规范和灵活性。

11. 为什么建议开发者谨慎使用继承?

在面向对象编程(OOP)中,
继承
是一种常见的代码复用方式,它允许一个类继承另一个类的属性和行为。然而,虽然继承可以提高代码的复用性,但过度或不当使用继承可能会导致代码的复杂性增加,进而带来一些潜在的问题。因此,建议开发者在使用继承时要谨慎,以下是一些关键原因:

1.
增加了类之间的耦合性

继承会导致子类和父类之间形成紧密的耦合关系。子类依赖于父类的实现,这意味着如果父类发生变化,可能会影响到所有继承自该父类的子类,导致修改和维护变得更加困难。这种紧密耦合关系也限制了子类的灵活性,因为它必须遵循父类的接口和实现。

例子

class Animal {
    void eat() {
        System.out.println("Animal is eating");
    }
}

class Dog extends Animal {
    @Override
    void eat() {
        System.out.println("Dog is eating");
    }
}

如果父类
Animal
做了改动(如修改
eat()
方法的实现),
Dog
类也会受到影响。这样的耦合会增加后期维护的复杂度。

2.
破坏了封装性(Encapsulation)

继承可能破坏封装性,因为子类可以直接访问父类的成员(字段和方法),尤其是当父类成员被设置为
protected

public
时。这种情况可能导致子类暴露不应被外界访问的细节,破坏了数据的封装性。

例子

class Vehicle {
    protected int speed;
}

class Car extends Vehicle {
    void accelerate() {
        speed += 10; // 直接访问父类的 protected 字段
    }
}

在这种情况下,
Car
类直接访问了父类
Vehicle

speed
字段,而不是通过公共接口来修改它,导致封装性降低。

3.
继承可能会导致类的层次结构不合理

继承往往会导致不合理的类层次结构,特别是在试图通过继承来表达“是一个”(
is-a
)关系时,实际情况可能并不符合这种逻辑。滥用继承可能会使类之间的关系变得复杂和不直观,导致代码结构混乱。

例子

假设我们有一个
Car
类和一个
Truck
类,都继承自
Vehicle
类。如果
Car

Truck
共享很多方法和属性,这样的设计可能是合适的。但是,如果
Car

Truck
之间差异很大,仅通过继承来构建它们的关系,可能会导致继承层次过于复杂,代码阅读和理解变得困难。

4.
继承可能导致不易发现的错误

由于子类继承了父类的行为,任何对父类的修改都有可能影响到子类的行为。更糟糕的是,错误或不一致的修改可能在父类中发生,而这些错误可能不会立即暴露出来,直到程序运行到某个特定的地方,才会显现出错误。

例子

假设你修改了父类的某个方法,但忘记更新或调整子类中相应的重写方法,这可能会导致难以发现的错误。

5.
继承限制了灵活性(不可重用性问题)

继承创建了一个父类与子类之间的固定关系,这意味着如果你想在一个完全不同的上下文中重用一个类,你可能不能通过继承来实现。在某些情况下,组合比继承更为灵活,允许你将多个行为组合到一个类中,而不是通过继承来强行构建类的层次结构。

例子

// 组合而非继承
class Engine {
    void start() {
        System.out.println("Engine started");
    }
}

class Car {
    private Engine engine = new Engine(); // 通过组合来使用 Engine
    void start() {
        engine.start();
    }
}

通过组合,可以灵活地使用不同的组件,而不需要继承整个类。这样做的优点是更具扩展性和灵活性。

6.
继承限制了方法的重用(可维护性差)

如果你过度依赖继承,你的代码会容易受到父类实现的限制,难以灵活地添加新功能或进行扩展。例如,在继承链中添加新的功能可能会导致一大堆方法的修改和重写,而不通过继承,可以更轻松地将功能作为独立模块来重用。

7.
使用接口和组合更优

相比继承,
接口(Interface)

组合(Composition)
更符合面向对象设计的原则。接口允许类只暴露所需的功能,而不暴露实现细节,组合则允许你将多个不同的行为组合在一起,使得系统更加灵活和可扩展。通过接口和组合,可以避免继承的许多问题。

推荐设计模式

  • 策略模式(Strategy Pattern)
    :通过接口和组合来替代继承。
  • 装饰器模式(Decorator Pattern)
    :使用组合和代理来扩展行为,而非通过继承。

小结

尽管继承是面向对象编程中的一个重要特性,但滥用继承可能带来许多问题,特别是在以下几个方面:

  • 增加类之间的耦合,降低灵活性;
  • 破坏封装性,暴露不应访问的内部实现;
  • 可能导致类层次结构复杂,增加理解和维护的难度;
  • 限制代码的重用和扩展性。

因此,
推荐优先使用组合而非继承
,并尽可能使用接口来实现灵活的扩展。如果必须使用继承,确保它能够清晰地表达“是一个”的关系,并避免过深的继承层次。

12. 为什么禁止开发人员修改 serialVersionUID 字段的值?

serialVersionUID
是 Java 中用来标识序列化版本的一个静态字段。它的作用是确保在反序列化时,JVM 可以验证序列化的类与当前类的兼容性,以避免版本不兼容导致的错误。尽管
serialVersionUID
可以由开发人员手动定义,
禁止开发人员修改
serialVersionUID
字段的值

的原因如下:

1.
序列化与反序列化兼容性

serialVersionUID
的主要作用是保证在序列化和反序列化过程中,类的版本兼容性。它是用来标识类的版本的,如果序列化和反序列化过程中使用的类的
serialVersionUID
不匹配,就会抛出
InvalidClassException

  • 不匹配的
    serialVersionUID

    会导致序列化的数据与当前类不兼容,导致反序列化失败。
  • 修改
    serialVersionUID

    的值会改变类的版本标识,导致已序列化的数据在反序列化时不能成功读取,特别是在类结构发生改变(例如添加或删除字段)时。

例如:

// 类的第一次版本
public class MyClass implements Serializable {
    private static final long serialVersionUID = 1L;
    private String name;
    // 其他字段和方法
}

// 类的第二次修改版本
public class MyClass implements Serializable {
    private static final long serialVersionUID = 2L;  // 修改了 serialVersionUID
    private String name;
    private int age;  // 新增字段
    // 其他字段和方法
}

如果修改了
serialVersionUID
,而之前序列化的数据是使用版本 1 的类进行序列化的,反序列化时会因为
serialVersionUID
不匹配而导致失败。

2.
避免不必要的版本冲突

Java 会根据类的字段、方法等信息自动生成
serialVersionUID
,这个值是基于类的结构计算出来的。如果开发人员修改了
serialVersionUID
,可能会破坏 Java 自动生成的版本控制机制,从而导致版本控制不一致,增加了维护复杂性。

如果手动修改
serialVersionUID
,容易出现以下几种问题:

  • 由于类结构没有变化,修改
    serialVersionUID
    可能会导致已序列化的数据无法恢复。
  • 如果不同的开发人员修改了
    serialVersionUID
    ,可能会在不同的机器或系统间引起序列化不一致。

3.
影响序列化兼容性

Java 提供了两种主要的兼容性规则:

  • 兼容性向前
    :如果类的字段或方法发生改变,但没有改变
    serialVersionUID
    ,则反序列化是可以工作的。
  • 兼容性向后
    :如果你修改了类的结构(如字段变动、方法签名改变等),并且保持相同的
    serialVersionUID
    ,反序列化仍然可以工作。

如果不小心修改了
serialVersionUID
,可能导致以下情况:

  • 向前兼容性
    :新版本的类不能兼容老版本的对象,导致反序列化失败。
  • 向后兼容性
    :老版本的类无法反序列化新版本的对象。

4.
自动生成 vs 手动指定

  • 自动生成的
    serialVersionUID

    :Java 会根据类的结构自动生成
    serialVersionUID
    ,这样如果类的结构发生变化,
    serialVersionUID
    会自动变化,确保不兼容的版本之间不会出现意外的反序列化行为。
  • 手动指定
    serialVersionUID

    :手动修改
    serialVersionUID
    可能导致版本控制不一致,特别是在多人开发、分布式部署的环境中,容易出现反序列化失败的问题。

5.
避免非预期的反序列化问题

手动修改
serialVersionUID
可能会导致数据丢失或反序列化时抛出异常。例如,如果开发人员错误地修改了
serialVersionUID
,系统在尝试反序列化时可能会因为
serialVersionUID
不匹配而无法成功加载对象,导致异常的发生。

小结

禁止开发人员修改
serialVersionUID
字段的值,主要是为了:

  • 确保序列化与反序列化的兼容性
    ,避免版本不匹配导致反序列化失败。
  • 避免不必要的版本冲突和数据丢失
    ,特别是在类结构修改时。
  • 保持 Java 自动管理
    serialVersionUID
    的优势

    ,保证类的版本一致性和可维护性。

如果确实需要修改
serialVersionUID
,应确保修改后的版本与已经序列化的数据兼容,并遵循合理的版本管理策略。

13. 为什么禁止开发人员使用 isSuccess 作为变量名?

禁止开发人员使用
isSuccess
作为变量名,主要是为了遵循更好的编程规范和提高代码的可读性、可维护性。这个变量名问题的核心在于其容易引起歧义和混淆。具体原因如下:

1.
不符合布尔值命名约定

在 Java 中,通常使用
is

has
开头的变量名来表示布尔值(
boolean
类型)。这类命名通常遵循特定的语义约定,表示某个条件是否成立。例如:

  • isEnabled
    表示某个功能是否启用;
  • hasPermission
    表示是否有权限。

问题

  • isSuccess
    看起来像一个布尔值(
    boolean
    类型),但它实际上可能并不直接表示一个布尔值,而是一个状态或结果。这种命名可能会导致混淆,开发者可能误以为它是布尔类型的变量,而实际上它可能是一个描述状态的对象、字符串或者其他类型的数据。

2.
语义不明确

isSuccess
这个名字表面上表示“是否成功”,但是它缺少具体的上下文,导致语义不够明确。真正表示是否成功的布尔值应该直接使用
boolean
类型的变量,并且使用清晰明确的命名。

例如:

  • isCompleted
    :表示某个任务是否完成。
  • isSuccessful
    :表示某个操作是否成功。

这些命名能更明确地表达布尔变量的含义,避免理解上的歧义。

3.
与标准的
is
前缀混淆

is
前缀通常用来表示“是否”某个条件成立,适用于返回布尔值的方法或者变量。
isSuccess
这样的命名会让开发人员误以为它是一个布尔值,或者一个
boolean
类型的值,但实际上它可能是一个复杂类型或者其他非布尔类型,造成不必要的混淆。

例如:

boolean isSuccess = someMethod(); // 看起来是布尔值,但实际类型可能不同

这种情况可能导致开发人员产生误解,认为
isSuccess
代表的是布尔值,但它可能是某个表示成功的对象、枚举或者其他数据类型。

4.
更好的命名建议

为了避免歧义和混淆,开发人员应使用更加明确且符合命名规范的名称。以下是一些命名的改进建议:

  • 如果是布尔值,命名为
    isSuccessful

    wasSuccessful
  • 如果是表示结果的对象,使用更具体的名称,例如
    operationResult

    statusCode
    ,以表明它是一个描述操作结果的变量。

5.
提升代码的可读性和可维护性

清晰且具有意义的命名能够帮助团队成员或未来的开发者更快地理解代码的意图。如果变量名过于模糊(如
isSuccess
),就可能让人对其实际含义产生疑问,尤其是在阅读较大或复杂的代码时。良好的命名能够提升代码的可读性和可维护性。

小结

  • isSuccess
    这样的命名不清晰,容易与布尔类型的变量产生混淆,进而影响代码的可读性。
  • 命名应尽量明确
    ,避免使用容易引起歧义的名称,特别是在布尔值类型的命名时。
  • 建议使用更具描述性的名称,如
    isSuccessful

    wasSuccessful
    ,更清晰地表达变量的意义。

最后

以上是 V 哥精心总结的13个 Java 编程中的小小编码问题,也是V 哥日常编码中总结的学习笔记,分享给大家,如果内容对你有帮助,请不要吝啬来个小赞呗,关注威哥爱编程,Java 路上,你我相伴前行。

一、入门案例

1. 添加依赖
首先,在 pom.xml 文件中添加 Spring Boot 和 Spring Event 的依赖:

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
</dependencies>

2. 定义自定义事件
创建一个自定义事件类 CustomEvent,继承自 ApplicationEvent:

packagecom.example.demo.event;importorg.springframework.context.ApplicationEvent;public class CustomEvent extendsApplicationEvent {privateString message;publicCustomEvent(Object source, String message) {super(source);this.message =message;
}
publicString getMessage() {returnmessage;
}
}

3. 创建事件监听器
创建一个事件监听器类 CustomEventListener,实现 ApplicationListener 接口:

packagecom.example.demo.listener;importcom.example.demo.event.CustomEvent;importorg.springframework.context.ApplicationListener;importorg.springframework.stereotype.Component;

@Component
public class CustomEventListener implements ApplicationListener<CustomEvent>{
@Override
public voidonApplicationEvent(CustomEvent event) {
System.out.println(
"Received custom event - " +event.getMessage());
}
}

4. 发布事件
在 DemoApplication 类中,注入 ApplicationEventPublisher 并发布自定义事件:

packagecom.example.demo;importcom.example.demo.event.CustomEvent;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.boot.CommandLineRunner;importorg.springframework.boot.SpringApplication;importorg.springframework.boot.autoconfigure.SpringBootApplication;importorg.springframework.context.ApplicationEventPublisher;

@SpringBootApplication
public class DemoApplication implementsCommandLineRunner {

@Autowired
privateApplicationEventPublisher publisher;public static voidmain(String[] args) {
SpringApplication.run(DemoApplication.
class, args);
}

@Override
public void run(String... args) throwsException {
CustomEvent event
= new CustomEvent(this, "Hello, Spring Event!");
publisher.publishEvent(event);
}
}

5. 运行应用
运行 DemoApplication 类,启动 Spring Boot 应用程序。你应该会在控制台看到以下输出:

收到:com.sun.springevent.SpringEventApplication$$SpringCGLIB$$0@6e3ecf5c消息;时间:1731466981066;消息:Hello, Spring Event!

二、Spring-Event执行原理

Spring Event 机制是 Spring 框架中的一个事件驱动模型,用于在应用程序的不同组件之间进行解耦和通信。下面是 Spring Event 的执行原理的详细解释:

1. 事件 (Event)
定义:事件是应用程序中发生的一个特定动作或状态变化。
实现:在 Spring 中,事件通常是由一个继承自 ApplicationEvent 的类表示的。例如,我们之前定义的 CustomEvent 就是一个自定义事件。
2. 事件发布者 (Event Publisher)
定义:事件发布者是负责创建并发布事件的组件。
实现:在 Spring 中,可以通过 ApplicationEventPublisher 接口来发布事件。通常,这个接口会通过依赖注入的方式注入到需要发布事件的类中。
3. 事件监听器 (Event Listener)
定义:事件监听器是负责处理事件的组件。
实现:在 Spring 中,可以通过实现 ApplicationListener 接口或者使用 @EventListener 注解来定义事件监听器。
4. 事件传播过程
1、事件创建:
事件发布者创建一个 ApplicationEvent 的实例,并传递必要的参数。
2、事件发布:
事件发布者调用 ApplicationEventPublisher 的 publishEvent 方法,将事件对象传递给 Spring 容器。
3、事件分发:
Spring 容器接收到事件后,会查找所有注册了该事件类型的监听器。
Spring 容器会调用每个监听器的 onApplicationEvent 方法(如果使用 ApplicationListener 接口)或带有 @EventListener 注解的方法。
4、事件处理:
监听器接收到事件后,根据事件的内容执行相应的业务逻辑。
5. 异步事件处理
默认行为:Spring 事件默认是同步处理的,即事件发布者会等待所有监听器处理完事件后才会继续执行后续代码。
异步处理:可以通过配置 ApplicationEventMulticaster 来实现异步事件处理。通常,可以在 application.properties 或 application.yml 中配置 TaskExecutor 来实现异步处理。
6. 示例代码解释
CustomEvent:定义了一个自定义事件类,继承自 ApplicationEvent,并添加了一个 message 属性。
CustomEventListener:定义了一个事件监听器类,实现了 ApplicationListener<CustomEvent> 接口,并重写了 onApplicationEvent 方法来处理事件。
DemoApplication:主类,实现了 CommandLineRunner 接口,在 run 方法中创建并发布了一个 CustomEvent 事件。


总结
Spring Event 机制通过事件发布者、事件监听器和事件本身,实现了组件之间的松耦合和灵活的事件驱动架构。通过这种方式,可以更容易地管理和扩展应用程序的功能。

三、深入地探讨publisher.publishEvent(event) 背后的具体执行流程和机制。

1. 事件发布方法 publishEvent

当你调用 ApplicationEventPublisher 的 publishEvent 方法时,Spring 容器会执行一系列操作来处理这个事件。以下是详细的步骤:

1.1 调用 publishEvent 方法

publisher.publishEvent(event);

1.2 AbstractApplicationContext 中的 publishEvent 方法

ApplicationEventPublisher 接口的实现类通常是 AbstractApplicationContext,它是 Spring 容器的核心类之一。

AbstractApplicationContext 中的 publishEvent 方法会调用 doPublishEvent 方法来处理事件。

public voidpublishEvent(ApplicationEvent event) {
assertContextActive();
getApplicationEventMulticaster().multicastEvent(event);
}

2. 事件多播器 ApplicationEventMulticaster

ApplicationEventMulticaster 是负责将事件广播给所有注册的监听器的组件。默认情况下,Spring 使用 SimpleApplicationEventMulticaster 实现。

2.1 multicastEvent 方法

SimpleApplicationEventMulticaster 的 multicastEvent 方法会遍历所有注册的监听器,并调用它们的 onApplicationEvent 方法。

@Overridepublic void multicastEvent(finalApplicationEvent event, @Nullable ResolvableType eventType) {
ResolvableType type
= (eventType != null ?eventType : resolveDefaultEventType(event));
Executor executor
=getTaskExecutor();for (ApplicationListener<?>listener : getApplicationListeners(event, type)) {if (executor != null) {
executor.execute(()
->invokeListener(listener, event));
}
else{
invokeListener(listener, event);
}
}
}

3. 事件监听器调用

3.1 invokeListener 方法

invokeListener 方法会调用具体的监听器方法来处理事件。

private void invokeListener(ApplicationListener<?>listener, ApplicationEvent event) {
ErrorHandler errorHandler
=getErrorHandler();if (errorHandler == null) {
doInvokeListener(listener, event);
}
else{try{
doInvokeListener(listener, event);
}
catch(Throwable err) {
errorHandler.handleError(err);
}
}
}
private void doInvokeListener(ApplicationListener<?>listener, ApplicationEvent event) {try{//调用监听器的 onApplicationEvent 方法 listener.onApplicationEvent(event);
}
catch(ClassCastException ex) {
String msg
=ex.getMessage();if (msg == null || matchesClassName(ex, event.getClass(), msg) ||matchesClassName(ex, ResolvableType.forInstance(event).getRawClass(), msg)) {
String className
=listener.getClass().getName();if(logger.isWarnEnabled()) {
logger.warn(
"Non-matching event type for listener [" + className + "]");
}
}
else{throwex;
}
}
}

4. 异步事件处理

4.1 配置 TaskExecutor

如果希望事件处理是异步的,可以在 application.properties 或 application.yml 中配置 TaskExecutor。例如:

spring:
task:
execution:
pool:
core
-size: 5max-size: 10queue-capacity: 100

4.2 自定义 ApplicationEventMulticaster

也可以通过自定义 ApplicationEventMulticaster 来实现更复杂的事件处理逻辑。例如:

@Configurationpublic classEventConfig {

@Bean
publicApplicationEventMulticaster applicationEventMulticaster() {
SimpleApplicationEventMulticaster eventMulticaster
= newSimpleApplicationEventMulticaster();
eventMulticaster.setTaskExecutor(
newSimpleAsyncTaskExecutor());returneventMulticaster;
}
}

5. 事件监听器的注册

5.1 通过 @Component 注解

可以使用 @Component 注解将监听器类注册为 Spring 管理的 Bean。

@Componentpublic class CustomEventListener implements ApplicationListener<CustomEvent>{
@Override
public voidonApplicationEvent(CustomEvent event) {
System.out.println(
"Received custom event - " +event.getMessage());
}
}

5.2 通过 @EventListener 注解

也可以使用 @EventListener 注解来定义事件监听器方法。

@Componentpublic classCustomEventListener {

@EventListener
public voidhandleCustomEvent(CustomEvent event) {
System.out.println(
"Received custom event - " +event.getMessage());
}
}

总结
通过上述步骤,我们可以看到 publisher.publishEvent(event) 背后的详细执行流程。Spring 容器通过 ApplicationEventMulticaster 将事件广播给所有注册的监听器,并调用它们的 onApplicationEvent 方法来处理事件。此外,Spring 还提供了异步事件处理和自定义事件多播器的能力,以满足更复杂的应用需求。