ASP.NET Core AOT

        Native AOT 最初在 .NET 7 中引入,在即将发布的 .NET 8 版本中可以与 ASP.NET Core 一起使用。在这篇文章中,我们从总体角度审视其优点和缺点,并进行测量以量化不同平台上的改进。

源代码:https://download.csdn.net/download/hefeng_aspnet/88689164

介绍
        .NET 8 版本即将发布,将为 ASP.NET Core带来一项新功能:本机提前编译,简称本机 AOT 。通常,在构建和部署 ASP.NET Core 应用程序时,C# 代码首先由 Roslyn(C# 编译器)编译为 Microsoft 中间语言 (MSIL)。在运行时,公共语言运行时 (CLR  ) 的即时编译器 (JIT) 将 MSIL 编译为其运行平台的本机代码(遵循著名的“编译一次,到处运行”原则)。本机 AOT 会将您的代码直接编译为选定目标平台的本机代码 - 运行时不再涉及 JIT。微软承诺更小的磁盘上的应用程序大小、更快的启动时间以及更少的执行期间的内存消耗。

        在这篇文章中,我们将从一般角度探讨本机 AOT 的优点和缺点,所以让我们深入研究它。

本土化
        首先,我创建一个.NET 8 ASP.NET Core 空 Web 应用程序项目,并 通过在 csproj 文件中设置PublishAot MSBuild 属性来激活本机 AOT 。我还禁用了<ImplicitUsings>,以便您可以看到引用代码的实际 using 语句,并在 <InvariantGlobalization>模式下运行,以消除特定于区域性的开销并简化部署 - 您可以在此处阅读有关它的更多信息。

        当您现在运行应用程序时(例如从 IDE 或使用 dotnet run),您将体验不到任何差异。本机 AOT 仅在您发布应用程序时编译为本机代码。在这篇介绍性文章中,我想进行一些测量以确定本机 AOT 和 JIT CLR 部署之间的差异。为此,我将稍微修改 Program.cs:

<Project Sdk="Microsoft.NET.Sdk.Web">
    <PropertyGroup>
        <TargetFramework>net8.0</TargetFramework>
        <Nullable>enable</Nullable>
        <PublishAot>true</PublishAot>
        <InvariantGlobalization>true</InvariantGlobalization>


        <DefineConstants Condition="'$(PublishAot)' == 'true'">
            $(DefineConstants);IS_NATIVE_AOT
        </DefineConstants>
    </PropertyGroup>
</Project>
using System;
using System.Diagnostics;
using Microsoft.AspNetCore.Builder;

long initialTimestamp = Stopwatch.GetTimestamp();

var builder =
#if IS_NATIVE_AOT
    WebApplication.CreateSlimBuilder(args);
#else
    WebApplication.CreateBuilder(args);
#endif

var app = builder.Build();
app.MapGet("/", () => "Hello World!");
await app.StartAsync();

TimeSpan elapsedStartupTime = Stopwatch.GetElapsedTime(initialTimestamp);
Console.WriteLine($"Startup took {elapsedStartupTime.TotalMilliseconds:N3}ms");

double workingSet = Process.GetCurrentProcess().WorkingSet64;
Console.WriteLine($"Working Set: {workingSet / (1024 * 1024):N2}MB");

await app.StopAsync();

        首先要注意的是在 Native AOT 模式下使用WebApplicaiton.CreateSlimBuilder而不是WebApplication.CreateBuilder – 这将减少默认注册的 ASP.NET Core 功能 数量,从而减少应用程序大小和内存使用量。我插入了对 Stopwatch.GetTimestamp和Stopwatch.GetElapsedTime的调用 来测量 Web 服务器的启动时间。 请注意,我还将调用从app.Run更改为app.StartAsync - 这样,我们不会阻塞执行线程,直到 Web 应用程序实际关闭,而只是直到 HTTP 服务器成功启动为止。此外,我们还获取进程的WorkingSet64 RAM 使用值并打印出已用内存(以兆字节为单位)。测量后,我们使用app.StopAsync正常关闭 Web 应用程序。  

        现在让我们发布应用程序两次,一次启用 Native AOT,一次在常规 CLR 模式下(只需在 csproj 文件旁边的 bash/终端中执行这些语句):

# If you follow along, please execute the following statements in the same "WebApp" folder

# This call will have PublishAot set to true because we defined it previously in the csproj file
dotnet publish -c Release -o ./bin/native-aot

# For this call, we will explicitly set PublishAot to false via cmd arguments
dotnet publish -c Release -o ./bin/regular-clr /p:PublishAot=false

         您会注意到,由于生成本机代码,第一个命令比第二个命令花费的时间更长。当您导航到 ./bin/native-aot 子文件夹并查看其内容时,您将看到调试符号 (.pdb) 和 appsettings.json 文件旁边只有一个 EXE 文件 – 所有 DLL已合并到可执行文件中,因为本机 AOT 涉及自包含、单文件部署和修剪。只需将其与常规 clr 子文件夹的内容进行比较,您就可以在较小的 EXE 文件旁边找到 DLL。

# On Ubuntu 22.04:
bin
├─ native-aot
│  ├─ WebApp  # Executable, 9.79MB
│  ├─ WebApp.dbg # debugging symbols, 33.02MB
│  └─ appsettings.json # 142B
└─ regular-clr
   ├─ WebApp # Executable, 70.96KB
   ├─ WebApp.deps.json # runtime and hosting information, 388B
   ├─ WebApp.dll # Compiled code of our project, 7KB
   ├─ WebApp.pdb # debgging symbols, 20.5KB
   ├─ WebApp.runtimeconfig.json # runtime information, 600B
   ├─ appsettings.json # 142B
   └─ web.config # IIS configuration, 482B

# On Windows 11
bin
└─ native-aot
   ├─ WebApp.exe  # Executable, 8.77MB
   ├─ WebApp.pdb # debugging symbols, 102.83MB
   └─ appsettings.json # 151B
# The regular-clr folder on Windows is omitted here, it contains the same files with roughly the same sizes

         设置应用程序后,我们现在可以对 Microsoft 声称在 Native AOT 中改进的内容进行一些测量:启动时间、内存使用情况和应用程序大小。请记住,所有这些值很大程度上取决于实际实现,例如您分配的对象数量、启动期间执行的语句以及您引用的框架和库。即将推出的图表仅类似于我们的小型简单应用程序 - 尽管如此,我们可以使用它来了解其优点以及它们在不同平台上的表现。

启动时间
        现在,我们可以多次执行应用程序以了解启动时间。我启动每个应用程序至少 10 次,然后对结果值取平均值。我的初始测试是在以下目标上执行的:

基于 Ubuntu-22.04-Jammy 的 Docker 容器(在基于 WSL 2 的引擎上运行)
基于 Alpine-3.18 的 Docker 容器(在基于 WSL 2 的引擎上运行)
WSL 2 上的 Ubuntu 22.04
Windows 11

        我们可以看到,Native AOT 的启动时间在 Linux 上约为 14 毫秒,在 Windows 11 上约为 17 毫秒。对于常规 CLR 部署,Ubuntu 上的启动时间约为 70 毫秒,Windows 上约为 80 毫秒。对于运行常规 CRL 构建的基于 Alpine-3.18 的 Docker 容器,存在大约 180 毫秒的巨大异常值,因此我在 Raspberry Pi 4 上重新执行了等效测试 - 这样我们还可以掌握功率较小的机器: 

        在 Raspberry Pi 4 的 ARM Cortex-A72 芯片上,Native AOT 启动时间仅不到 90ms。常规 CLR 构建的启动时间约为 530 毫秒。我们仍然有一个异常值,在 Alpine 3.18 Docker 容器中运行的常规 CLR 构建大约需要 600 毫秒(我还在 Azure 中的专用 Ubuntu 22.04 VM 上检查了这一点,并得到了与 Windows 上相同的结果)。

        总体而言,结果表明本机 AOT 启动时间比使用带有 JIT 的常规 CLR 运行速度大约快四到六倍(如果我们计算出 Alpine 异常值)。

内存使用情况
        让我们看一下 ASP.NET Core 应用程序的内存消耗:

 

        正如我们所看到的,根据底层架构的不同,结果会略有不同。对于本机 AOT,基于 ARM 的应用程序需要的内存量最少,约为 17MB 到 18MB,而在基于 x64 的 Linux 上,工作集范围约为 19MB 到 21MB。Windows x64 的最大工作集接近 23MB。

        与常规 CLR 构建相比,这些仍然是很大的改进:在 Linux 上,工作集减少了 50% 以上,因为常规 CLR 构建占用超过 50MB 的内存。在 Windows 上,CLR JIT 内存消耗略低于 40MB,因此收益并没有那么大。请记住,这些只是启动 Kestrel(ASP.NET Core 的 HTTP 服务器)后的数字 - 这些值将根据您在程序执行期间执行的分配而有很大差异(.NET 垃圾收集器没有任何变化)本机 AOT)。

应用大小

        那么应用程序的大小又如何呢?调用 dotnetpublish 后应用程序有多大?

        仅编译为 MSIL 会产生非常小的二进制文件大小,如上图所示。常规 CLR 部署在 Linux 上的大小仅为 82kB 左右,在 Windows 上为 145kB。相比之下,本机 AOT 在 Linux 上大约需要 10MB,在 Windows 上大约需要 9MB。然而,Native AOT 完全包含了所有必需的运行时组件,因此当我们添加实际的依赖项来运行我们的应用程序时,情况会发生巨大变化。让我们看一下 Docker 镜像的大小:

        在上图中,我们看到,特别是基于 Alpine 的 Docker 镜像在使用 Native AOT 时非常小。对于包含 我们的准系统 ASP.NET Core 应用程序的图像来说,大约 18MB 是一个惊人的值,大约是常规 CLR 构建的图像大小的 15%。基于 Ubuntu 的镜像也有类似的情况:它们的大小在 80MB 到 90MB 之间,不到常规 216MB 镜像大小的 40%。所有这些都减少了部署的有效负载,尤其是在云原生场景中。

用于本机 AOT 的 Dockerfile
        您可能想知道如何自己为 Native AOT 构建上述基于 Alpine 的 Dockerfile?让我们看一下:

FROM alpine:3.18 AS prepare
WORKDIR /app
RUN adduser -u 1000 --disabled-password --gecos "" appuser && chown -R appuser /app
USER appuser

FROM mcr.microsoft.com/dotnet/sdk:8.0.100-rc.2-alpine3.18 AS build
RUN apk update && apk upgrade
RUN apk add --no-cache clang build-base zlib-dev
WORKDIR /code
COPY ./WebApp.csproj .
ARG RUNTIME_ID=linux-musl-x64
RUN dotnet restore -r $RUNTIME_ID
COPY . .
RUN dotnet publish \
    -c Release \
    -r $RUNTIME_ID \
    -o /app \
    --no-restore

FROM prepare AS final
COPY --chown=appuser --from=build /app/WebApp ./WebApp
ENTRYPOINT ["./WebApp"]

        在第一个“准备”阶段,我们将从基本的 Alpine 3.18 镜像开始,该镜像默认使用 root 用户。为了避免以特权运行我们的 Web 应用程序,我们首先创建一个 /app 目录,编译后的 Web 应用程序将复制到其中,然后运行 ​​adduser 命令来创建具有较少权限的标准用户。我们使用以下参数:

-u 1000 设置新用户的ID
–disabled-password 不再需要新用户的密码
–gecos “”是设置帐户的一些常规信息(如“真实姓名”或“电话号码”)的简短方法 – 由于我们只传递空字符串,相应的字段将被初始化为空,操作系统不会询问这些信息用户首次登录时的信息
        然后,我们立即使用chown -R appuser /app将 app 文件夹的权限移交给这个新用户,并使用USER appuser切换到它。

然后是重要的“构建”阶段,我们使用 Native AOT 发布应用程序。我们从 mcr.microsoft.com/dotnet/sdk:8.0.100-rc.2-alpine3.18映像开始(.NET 8 正式发布后,您很可能可以使用 mcr.microsoft.com/dotnet/sdk :8.0-alpine3.18 映像,请参阅  Microsoft Artifact Registry了解详细信息)并使用apk update && apk Upgrade更新和升级 Alpine 的 APK 包管理器的存储库,以安装 clang、build-base 和 zlib-dev。在 Alpine 上构建本机 AOT 需要这些包 - 您可以在这篇 Microsoft Learn 文章中找到有关本机 AOT 构建先决条件的信息。

        最后,我们恢复NuGet包并跨多个层发布,以便可以缓存第一步以减少重建图像所需的时间。我们只需复制 csproj 文件,然后对其调用dotnet Restore ,并使用$RUNTIME_ID构建参数传入运行时标识符。默认情况下,它设置为 linux-musl-x64,但您可以轻松地将其更改为 linux-musl-arm64 来构建基于 arm 的系统。然后,我们复制其余的源文件并调用dotnetpublish在一个 Dockerfile 层中以发布模式构建和发布整个应用程序 - 请记住,PublishAot在我们的 csproj 文件中已打开。  

        然后,发布的本机 AOT 应用程序驻留在 /app 顶级文件夹中。我们可以简单地使用COPY –chown=appuser –from=build /app/WebApp ./WebApp命令将其从构建阶段复制到最后阶段 (该命令还将所有权一次性转移给 appuser)。请注意,我们仅复制单个可执行文件,而不是 .dbg 文件,也没有 appsettings.json 文件(您希望使用环境变量来配置应用程序)。此外,我们的本机 AOT 部署没有进一步的运行时依赖项- 我们的应用程序在普通的 Alpine 3.18 映像上运行,只需复制并执行二进制文件即可。

缺点
        因此,我们看到启动时间、内存使用量和 Docker 映像大小显着减少。我们不再需要打包任何运行时依赖项。我们现在应该切换到 Native AOT 吗?可能不会,因为您将放弃 .NET 生态系统中您可能了解和喜爱的许多功能。

        最重要的是:你不能使用未绑定的反射。本机 AOT 应用程序在发布期间会被修剪,即静态代码分析器将检查所有程序集中的类型及其成员,并确定它们是否被调用。这样做是因为本机代码指令比 MSIL 指令占用更多的空间,因此获得合理的二进制大小的唯一方法是在本机代码生成期间删除未使用的代码。

        不幸的是,这有 一些警告,其中一些是:

        1、不使用未绑定的反射,例如基于反射的 DTO 反序列化
        2、没有运行时代码生成
        3、不动态加载程序集(即不支持插件式应用程序)
        

        静态代码分析器在很大程度上取决于代码的调用结构,解决诸如反射调用之类的复杂问题既不容易完成,也无法在合理的时间范围内完成。然而,这导致许多依赖反射的框架与本机 AOT 部署不兼容:
 
        1、无法使用像 Newtonsoft.Json 这样在序列化和反序列化过程中依赖反射的序列化器。DTO 的 JSON 序列化仅适用于 System.Text.Json 与 Roslyn Source Generators 的结合。这与 IConfiguration 的反序列化是一样的:微软引入了 一个专用的源生成器 ,它发出反序列化代码以避免未绑定的反射。
        2、同样,像 Entity Framework Core 和 Dapper 这样的对象/关系映射器 (ORM) 依赖使用未绑定反射来实例化实体并填充其属性,它们也不与本机 AOT 兼容。他们都计划添加对源生成器的支持,但这在 .NET 8 发布时还没有准备好。
        3、不支持 ASP.NET Core MVC 和 SignalR – 仅最小 API 和 GRPC 可用于创建端点和流数据。有一个最小 API 源生成器,可以使用 MapXXX 方法处理端点注册。
        4、不直接支持本机代码的自动化测试:像 xunit 和 nunit 这样的单元测试框架都无法在本机 AOT 模式下运行。目前,.NET 团队专注于 分析器方法 ,因此当您构建应用程序并运行测试时,Roslyn 分析器将创建警告,提示在本机 AOT 模式下运行时的潜在问题。请注意,当您在 IDE 中使用 dotnet run、dotnet test 或类似功能时,您的应用程序将以常规 CLI 模式运行 - 本机 AOT 仅在您的应用程序发布时才会生效。
        5、本机代码的有限调试:发布后,您的应用程序是真正的本机二进制文件,因此 C# 的托管调试器将无法使用它。部署后我们需要使用gdb或WinDbg等调试器。您还需要调试符号(.dbg 或 .pdb 文件)以获得有用的调试体验。


        这些只是 Native AOT 的主要缺点。一般来说,您需要检查您想要合并的每个框架或库的本机 AOT 兼容性。严重依赖反射的流行框架需要在编译时用某种形式的代码生成替换此代码才能兼容。可以在此处找到本机 AOT 支持的 ASP.NET Core 功能列表。


结论
        本机 AOT 是 ASP.NET Core 的一个有趣的开发,它可能会在即将发布的版本(例如 .NET 9 和 10)中获得更多关注。主要目的是在云本机场景中,您更喜欢具有较小图像大小和内存占用的微服务,并且减少启动时间。对于需要快速启动的无服务器功能也是如此——在这些场景中,Native AOT 可以大放异彩。在常规 CLR 模式下运行时,JIT 在启动时需要编译大量代码(尤其是静态方法/成员),而在 Native AOT 模式下可以完全省略。但不要忘记:Native AOT 生成的代码不一定更快。事实上,分层 JIT 编译过程可以考虑目标系统(因为它就在您的代码旁边运行)——这是编译时创建的本机代码无法考虑的。

        库和框架仍然需要赶上原生 AOT 支持。当前依赖于未绑定反射的代码必须转换为编译时生成的代码,很可能是通过源生成器。一旦第三方框架和库支持它,开发人员是否会采用这种部署模型将会很有趣——尤其是 Entity Framework Core 在这里很重要。接下来的几年也将非常有趣,如何进一步调整 Native AOT 以与 Go 和 Rust 等语言竞争。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/288359.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

华清远见作业第二十天——IO(第三天)

思维导图&#xff1a; 使用标准IO完成两个文件的拷贝 代码&#xff1a; #include<stdio.h> #include<string.h> #include<stdlib.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> int main(int argc, const char *…

SQL Server注入之攻防技战法

那天下着很大的雨&#xff0c;母亲从城里走回来的时候&#xff0c;浑身就是一个泥人&#xff0c;那一刻我就知道我没有别的选择了 1.Mssql报错注入 0.判断数据库类型 1.爆当前用户名 2.爆版本 3.爆服务器名 4.判断数据库个数 5.获取全部数据库 语句只适合>2005 爆当前数据…

桶式移位器

前言 本篇文章介绍CPU的核心部件之一&#xff1a;桶式移位器&#xff0c;简称BS&#xff0c;英文全称为Barrel Shifter 桶式移位器最大的特点就是能在单周期内完成多种方式&#xff0c;各种位数的移位操作 常见的移位操作 常见的移位操作种类如下&#xff1a; 算术右移 是指…

google-java-format 配置及应用

以google 的java 风格为基准&#xff0c;以后面的开发中&#xff0c;都需要满足这个&#xff0c;主要用到的是google的两个插件&#xff1a;google-java-format, google_checks.xml代码格式检测&#xff0c;这两个堪称双剑合并&#xff0c;代码成诗。google-java-format替换ide…

RS485数据采集模块,如何一次采集多个modbus设备数据?

在工业数据采集中&#xff0c;RS485是一种常见的数据通信协议&#xff0c;而Modbus则是其上的常用设备协议。那么&#xff0c;如何用一个模块高效采集多个Modbus设备的数据呢&#xff1f;这就是我们今天要探讨的话题&#xff01; 什么是RS485数据采集模块&#xff1f; 首先&a…

算法28:力扣64题,最小路径和------------样本模型

题目&#xff1a; 给定一个二维数组matrix&#xff0c;一个人必须从左上角出发&#xff0c;最后到达右下角 。沿途只可以向下或者向右走&#xff0c;沿途的数字都累加就是距离累加和 * 返回累加和最小值 思路&#xff1a; 1. 既然是给定二维数组matrix&#xff0c;那么二维数…

FreeRTOS——队列及其实战

1.队列概念 1&#xff09;队列是任务到任务、任务到中断、中断到任务数据交流的一种机制&#xff08;消息传递&#xff09; 2&#xff09;队列类似数组&#xff0c;只能存储有限数量、相同类型的数据&#xff0c;在创建时需指定队列长度与队列项大小 3&#xff09;出队入队阻塞…

xshell登录不上虚拟机了

电脑重启后连不上本地虚机了 1、关闭防火墙 2 虚拟机ping得到主机&#xff0c;而主机ping不到虚拟机的解决办法 原因&#xff1a;可能是主机的网络适配器没有调好 首先&#xff0c;找到虚拟机的网络配置器 根据虚拟机的IP信息修改主机虚拟适配器VMnet8 修改ip使得和虚拟机连…

Element-ui自定义input框非空校验

1、vue自定义非空指令&#xff1a; main.js中自定义非空指令 当input框或下拉框中数据更新时&#xff0c;触发校验 Vue.directive(isEmpty,{update:function(el,binding,vnode){if(vnode.componentInstance.value""){el.classList.add("is-required");}e…

[Unity]实时阴影技术方案总结

一&#xff0c;Planar Shadow 原理就是将模型压扁之后绘制在需要接受阴影的物体上&#xff0c;这种方式十分高效&#xff0c;消耗很低。具体实现过程参考Unity Shader - Planar Shadow - 平面阴影。具按照自己的理解&#xff0c;其实就是根据光照方向计算片元在接受阴影的平面…

详解卡尔曼滤波(Kalman Filter)

1. 从维纳滤波到卡尔曼滤波 黑盒&#xff08;Black Box&#xff09;思想最早由维纳&#xff08;Wiener&#xff09;在1939年提出&#xff0c;即假定我们对从数据到估计中间的映射过程一无所知&#xff0c;仅仅用线性估计&#xff08;我们知道在高斯背景下&#xff0c;线性估计…

计算机创新协会冬令营——暴力枚举题目01

首先是欢迎大家参加此次的冬令营&#xff0c;我们协会欢迎所有志同道合的同学们。话不多说&#xff0c;先来看看今天的题目吧。 题目 力扣题号&#xff1a;2351. 第一个出现两次的字母 注&#xff1a;下述题目和示例均来自力扣 题目 给你一个由小写英文字母组成的字符串 s &…

RocketMQ5.0Pop消费模式

前言 RocketMQ 5.0 消费者引入了一种新的消费模式&#xff1a;Pop 消费模式&#xff0c;目的是解决 Push 消费模式的一些痛点。 RocketMQ 4.x 之前&#xff0c;消费模式分为两种&#xff1a; Pull&#xff1a;拉模式&#xff0c;消费者自行拉取消息、上报消费结果Push&#x…

探索Allure Report:提升自动化测试效率的秘密武器

亲爱的小伙伴们&#xff0c;由于微信公众号改版&#xff0c;打乱了发布时间&#xff0c;为了保证大家可以及时收到文章的推送&#xff0c;可以点击上方蓝字关注测试工程师成长之路&#xff0c;并设为星标就可以第一时间收到推送哦&#xff01; 一.使用 Allure2 运行方式-Python…

【操作系统xv6】学习记录4 -CPU上下文:进程上下文、线程上下文、中断上下文

什么是cpu上下文 CPU 寄存器和程序计数器就是 CPU 上下文&#xff0c;因为它们都是 CPU 在运行任何任务前&#xff0c;必须的依赖环境。 什么是 CPU 上下文切换 先把前一个任务的 CPU 上下文&#xff08;也就是 CPU 寄存器和程序计数器&#xff09;保存起来&#xff0c;然后…

equals()比较字符串和MySQL中=比较结果不一致

问题&#xff1a; 普通车辆入园统计结果数量和普通车辆统计列表数量不一致&#xff1f; 列子&#xff1a;数量:967&#xff0c;列表:974 解决问题步骤 对比统计数量和统计列表的统计方法 统计数量代码实现 一&#xff1a;查询出车辆滞留表数据List 二&#xff1a;查询出…

112. 雷达设备(贪心/逆向思考)

题目&#xff1a; 112. 雷达设备 - AcWing题库 输入样例&#xff1a; 3 2 1 2 -3 1 2 1输出样例&#xff1a; 2 思路&#xff1a; 代码&#xff1a; #include <cstdio> #include <cstring> #include <iostream> #include <algorithm> #include<…

海外住宅IP代理的工作原理和应用场景分析,新手必看

海外住宅IP代理作为一种技术解决方案&#xff0c;为用户提供了访问全球网络资源和维护隐私安全的方法。本文将介绍海外住宅IP代理的工作原理和应用场景&#xff0c;帮助读者更好地理解和利用这一技术。 一、工作原理 海外住宅IP代理的工作原理基于代理服务器和IP地址的转发。它…

【springboot配置文件加载源码分析】

在Spring Boot的源码中&#xff0c;配置文件的加载是在应用程序启动的早期阶段进行的。具体来说&#xff0c;配置文件加载的主要步骤发生在SpringApplication类的run()方法中的prepareEnvironment方法中&#xff0c;真正读取我们的配置文件还是PropertySourceLoader。 本篇博客…

Docker安装Flarum(开源论坛)

Flarum介绍安装命令 #---------------------------------------------------------- mkdir -p /opt/flarum && cd /opt/flarum #---------------------------------------------------------- docker run -p 8888:8888 --name flarum \ --restartalways \ -v /opt/flar…