news 2026/9/11 21:36:38

Diffusers 组件管理器(ComponentsManager)完全指南:跨管道的模型注册、重复检测与自动卸载

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Diffusers 组件管理器(ComponentsManager)完全指南:跨管道的模型注册、重复检测与自动卸载

Diffusers 组件管理器(ComponentsManager)完全指南:跨管道的模型注册、重复检测与自动卸载

【免费下载链接】diffusers🤗 Diffusers: State-of-the-art diffusion models for image, video, and audio generation in PyTorch.项目地址: https://gitcode.com/GitHub_Trending/di/diffusers

导读

本指南聚焦 🤗 Diffusers Modular Diffusers 体系中的核心基础设施——[ComponentsManager],讲解如何在一个集中式注册表中统一管理跨多条 Modular Pipeline 共享的模型组件(UNet、VAE、文本编码器、调度器等),以及如何借助它实现组件元数据跟踪、重复实例检测、集合(collection)组织和全局自动 CPU 卸载。读完本文,你将掌握ComponentsManagerModularPipeline的完整协作方式,能够编写出在多管道场景下复用模型、按需换入换出、并自动管理设备内存的实战代码。

说明:ComponentsManager当前仍属于实验性功能,接口在未来版本中可能调整(源码类注释中明确标注了这一点,参见 components_manager.py 中的> [!WARNING]提示)。文中所有 API 行为均以当前仓库源码为准。


一、组件管理器是什么

[ComponentsManager] 是 Modular Diffusers 的模型注册与管理中枢。它的核心职责可以概括为四点:

  1. 添加与跟踪模型:把任意组件(torch.nn.Module或调度器、引导器等其他对象)以唯一 ID 注册到统一注册表中;
  2. 存储有用元数据:记录组件所属集合、模型大小(GB)、当前设备放置、执行设备、适配器(如 LoRA adapters)、IP-Adapter 缩放、量化配置等信息;
  3. 防止重复模型实例:即使不同的 Python 对象代表同一个底层检查点,也能通过load_id自动识别并发出警告;
  4. 支持自动卸载:提供跨所有模型的全局 CPU 卸载策略,无需逐个管道手动管理设备。

从源码看,ComponentsManager内部使用OrderedDict维护三份映射(见 components_manager.py):

  • self.componentscomponent_id -> component的主注册表;
  • self.added_time:每个组件被加入的时间戳;
  • self.collectionscollection_name -> set of component_ids的集合索引。

此外还有model_hooks(卸载钩子列表)以及_auto_offload_enabled/_offload_strategy两个卸载状态字段。_available_info_fields定义了get_model_info可查询的元数据字段清单:model_idadded_timecollectionclass_namesize_gbadaptershas_hookexecution_deviceip_adapterquantization

ComponentsManager通常与ModularPipeline一起创建使用,但也可以独立实例化(comp = ComponentsManager())后手动添加组件,二者是松耦合关系。


二、添加组件:与 ModularPipeline 的两种协作方式

ComponentsManager应在ModularPipeline.from_pretrainedModularPipelineBlocks.init_pipeline时传入,从而让管道在注册组件时自动把组件交给管理器托管。

[!TIP]collection参数是可选的,但可以更轻松地组织和管理组件。给组件打上集合标签后,后续可以按集合批量检索、替换与清理。

方式一:from_pretrained

from diffusers import ModularPipeline, ComponentsManager comp = ComponentsManager() pipe = ModularPipeline.from_pretrained("YiYiXu/modular-demo-auto", components_manager=comp, collection="test1")

ModularPipeline.from_pretrained的签名中支持components_managercollection两个参数(见 modular_pipeline.py)。从源码看,管道在内部会把components_manager存入self._components_manager,把collection存入self._collection,并在注册每个from_pretrained组件时调用self._components_manager.add(name, module, self._collection)自动完成注册(见 modular_pipeline.py)。

方式二:init_pipeline

from diffusers import ComponentsManager from diffusers.modular_pipelines import SequentialPipelineBlocks from diffusers.modular_pipelines.stable_diffusion_xl import TEXT2IMAGE_BLOCKS t2i_blocks = SequentialPipelineBlocks.from_blocks_dict(TEXT2IMAGE_BLOCKS) modular_repo_id = "YiYiXu/modular-loader-t2i-0704" components = ComponentsManager() t2i_pipeline = t2i_blocks.init_pipeline(modular_repo_id, components_manager=components)

SequentialPipelineBlocks.init_pipeline会通过MODULAR_PIPELINE_MAPPING找到对应的管道类,然后以components_manager=components_manager, collection=collection为参数实例化ModularPipeline(见 modular_pipeline.py)。

组件何时真正加载

关键点在于:组件并非在from_pretrained/init_pipeline时立刻加载。对于default_creation_method="from_pretrained"的组件,管道初始化时只保存组件规格(ComponentSpec),组件属性被置为None;真正加载发生在调用pipe.load_components()时(或后续通过update_components手动注入)。

load_components的签名支持按需加载(见 modular_pipeline.py):

  • names=None:加载所有default_creation_method == "from_pretrained"且尚未加载的组件;
  • names传入字符串或列表:只加载指定组件;
  • workflow:只加载某个工作流执行块实际用到的组件(namesworkflow不能同时传);
  • **kwargs:额外的加载参数,可以是单个值(应用到所有组件,如dtype=torch.bfloat16),也可以是字典(如dtype={"unet": torch.bfloat16, "default": torch.float32}),甚至可以覆盖ComponentSpec中的加载字段(如pretrained_model_name_or_pathvariantrevision)。

下面的示例演示了多管道共享组件的典型模式:先加载第一个管道,再创建第二个管道并复用第一个管道的所有组件,只是把它们归入不同的集合。

pipe.load_components() pipe2 = ModularPipeline.from_pretrained("YiYiXu/modular-demo-auto", components_manager=comp, collection="test2")

识别缺失组件并补齐

新管道pipe2尚未加载任何组件。可以使用pipe2.null_component_names属性找出所有尚未加载的组件名(源码实现为:遍历_component_specs,返回所有当前属性值为None的组件名,见 modular_pipeline.py),再通过comp.get_components_by_names从管理器检索,最后用pipe2.update_components注入:

pipe2.null_component_names ['text_encoder', 'text_encoder_2', 'tokenizer', 'tokenizer_2', 'image_encoder', 'unet', 'vae', 'scheduler', 'controlnet'] comp_dict = comp.get_components_by_names(names=pipe2.null_component_names) pipe2.update_components(**comp_dict)

update_components(**kwargs)不仅可以替换组件,还可以更新配置值(ConfigSpec的默认值),并会把传入的非模块配置重新注册进管道配置(见 modular_pipeline.py)。

手动添加与移除单个组件

要手动添加单个组件,使用ComponentsManager.add。它会用{name}_{id(component)}的格式生成唯一组件 ID,其中id(component)是 Python 内建的对象唯一标识符(见 components_manager.py)。

from diffusers import AutoModel text_encoder = AutoModel.from_pretrained("stabilityai/stable-diffusion-xl-base-1.0", subfolder="text_encoder") component_id = comp.add("text_encoder", text_encoder) comp

add的内部逻辑值得注意:

  1. 对象级重复检测:遍历已有组件,如果发现comp == component(同一对象),且基础名称相同,则直接复用原组件 ID 并发出警告,不新增条目;
  2. load_id 重复检测:如果组件带_diffusers_load_id且不为"null",会查询拥有相同 load_id 的已有组件并发出警告(但不会自动删除,需要用户手动remove);
  3. 集合替换:如果指定了collection,且该集合中已存在同名组件,会先调用remove_from_collection把旧组件移出该集合(若旧组件不属于任何其他集合,则会被彻底移出管理器);
  4. 自动卸载联动:如果已启用自动卸载且是新组件,会重新执行一次enable_auto_cpu_offload为该组件挂上卸载钩子。

comp直接打印(__repr__)会输出一个可读的组件表格,包含 Models 与 Other Components 两个分区,展示Name_IDClassDevice: act(exec)DtypeSize (GB)Load IDCollection等列(见 components_manager.py)。

使用remove通过 ID 移除组件。移除时它会从所有集合中摘除该组件,释放引用并执行gc.collect(),在 CUDA/XPU 可用时还会调用torch.cuda.empty_cache()/torch.xpu.empty_cache()清理显存缓存(见 components_manager.py):

comp.remove("text_encoder_139917733042864")

三、检索组件:get_one 与 get_components_by_names

ComponentsManager提供了多套检索 API。get_one用于精确定位单个组件,get_components_by_names用于按名称批量取回并直接对接update_components,此外还有get_idsget_components_by_idssearch_components等底层能力(见 components_manager.py 与 components_manager.py)。

get_one:支持模式匹配的单组件检索

get_one支持对name参数做模式匹配(也支持直接传component_id精确取回,或按collectionload_id过滤)。如果多个组件匹配,get_one会抛出ValueError;如果没有匹配,同样会抛出ValueError(见 components_manager.py)。

模式示例描述
exactcomp.get_one(name="unet")精确名称匹配
wildcardcomp.get_one(name="unet*")名称以 "unet" 开头(前缀通配)
exclusioncomp.get_one(name="!unet")排除名为 "unet" 的组件(取反)
orcomp.get_one(name="unet|vae")名称为 "unet" 或 "vae"(OR 组合)

底层search_components的模式语法比表格更丰富(见 components_manager.py):

  • "unet":精确匹配基础名称为unet的组件(如unet_123abc,ID 去掉尾部数字后缀即为基础名称);
  • "!unet":除unet以外的所有组件;
  • "unet*":基础名称以unet开头的组件;"!unet*"为取反;
  • "*unet*":基础名称包含unet的组件;"!*unet*"为取反;
  • "refiner|vae|unet":基础名称精确等于三者之一的组件;"!refiner|vae|unet"为取反;
  • "unet*|vae*":基础名称以unet开头vae开头的组件。

模式匹配是基于基础名称_id_to_name:去掉component_id中最后一个_之后的后缀)进行的,而不是完整的组件 ID,因此不受id(component)数字后缀干扰。get_one还支持通过collection参数或load_id参数过滤:

comp.get_one(name="unet", collection="sdxl")

注意get_one的约束:如果通过component_id检索,就不能同时传namecollectionload_id,否则会抛ValueError(见 components_manager.py)。

get_components_by_names:批量检索并直接喂给管道

get_components_by_names(names, collection=None)接受名称列表,返回「名称 -> 组件」的字典。这在ModularPipeline中特别有用:管道提供所需组件名列表(如null_component_names),返回的字典可直接解包传给update_components(见 components_manager.py)。

component_dict = comp.get_components_by_names(names=["text_encoder", "unet", "vae"]) {"text_encoder": component1, "unet": component2, "vae": component3}

注意:当以名称为键返回时,如果检索结果中出现重名组件会抛出ValueError,此时可以通过get_components_by_ids(ids, return_dict_with_names=False)改用组件 ID 作为键返回,避免歧义(见 components_manager.py)。


四、重复检测:用 ComponentSpec 消除重复模型实例

在多管道、多检查点切换的场景中,同一个模型(例如 SDXL 的text_encoder)很容易被加载多次,白白浪费内存。ComponentsManager建议使用 [ComponentSpec] 加载模型组件:ComponentSpec会为加载的组件打上_diffusers_load_id标记,该标记编码了加载参数,管理器据此自动识别重复。

ComponentSpec 与 load_id

ComponentSpec是描述组件的规格类(见 modular_pipeline_utils.py),核心字段包括:

  • name:组件名称;
  • type_hint:组件类型(如UNet2DConditionModelCLIPTextModel),用于from_pretrained/from_single_file加载;
  • description:可选描述;
  • configfrom_config创建方式使用的配置字典;
  • pretrained_model_name_or_path:预训练仓库或路径(旧字段repo已废弃,若传repo会自动转写到pretrained_model_name_or_path);
  • subfoldervariantrevision:加载参数;
  • default_creation_method"from_config""from_pretrained",默认"from_pretrained"

其中load_id属性是关键:它把加载字段拼接成pretrained_model_name_or_path|subfolder|variant|revision的字符串(None段用"null"表示),from_config方式创建的组件load_id"null"(见 modular_pipeline_utils.py)。ComponentSpec.load()在成功加载后会设置component._diffusers_load_id = self.load_id(见 modular_pipeline_utils.py)。

实战示例:重复加载同一检查点

from diffusers import ComponentSpec, ComponentsManager from transformers import CLIPTextModel comp = ComponentsManager() # 为第一个文本编码器创建 ComponentSpec spec = ComponentSpec(name="text_encoder", repo="stabilityai/stable-diffusion-xl-base-1.0", subfolder="text_encoder", type_hint=AutoModel) # 为重复的文本编码器创建 ComponentSpec(它是相同的检查点,来自相同的仓库/子文件夹) spec_duplicated = ComponentSpec(name="text_encoder_duplicated", repo="stabilityai/stable-diffusion-xl-base-1.0", subfolder="text_encoder", type_hint=CLIPTextModel) # 加载并添加两个组件 - 管理器会检测到它们是同一个模型 comp.add("text_encoder", spec.load()) comp.add("text_encoder_duplicated", spec_duplicated.load())

这会返回一个警告,附带移除重复项的说明:

ComponentsManager: adding component 'text_encoder_duplicated_139917580682672', but it has duplicate load_id 'stabilityai/stable-diffusion-xl-base-1.0|text_encoder|null|null' with existing components: text_encoder_139918506246832. To remove a duplicate, call `components_manager.remove('<component_id>')`. 'text_encoder_duplicated_139917580682672'

警告信息中可以看到两个关键细节:

  1. load_id 结构stabilityai/stable-diffusion-xl-base-1.0|text_encoder|null|null对应pretrained_model_name_or_path|subfolder|variant|revision四个段;
  2. 行为是警告而非自动删除add()只做记录与提示,重复实例依然会被注册,需要你手动调用remove('<component_id>')清理。

什么时候检测会失效

你也可以不用ComponentSpec添加组件,多数情况下即使以不同名称添加同一对象,重复检测仍然有效——因为add()内部会对comp == component(对象相等性)做检查。

然而,当相同组件被加载到不同对象时(例如分别调用两次AutoModel.from_pretrained),ComponentsManager无法仅凭对象判断二者是同一个模型。此时应使用ComponentSpec加载模型,让 load_id 机制接管:

text_encoder_2 = AutoModel.from_pretrained("stabilityai/stable-diffusion-xl-base-1.0", subfolder="text_encoder") comp.add("text_encoder", text_encoder_2) 'text_encoder_139917732983664'

另外还有一个便捷方法ComponentSpec.from_component(name, component),可以从已加载的组件反推规格:支持ComponentSpec.load()创建的组件,以及非nn.ModuleConfigMixin子类(如调度器、引导器,会用from_config方式重建);如果传入一个未经过ComponentSpec.load()创建的nn.Module,则会抛出ValueError(见 modular_pipeline_utils.py)。


五、集合(Collection):跨节点的组件组织与自动替换

集合是为组件分配的标签,用于更好的组织和管理。使用ComponentsManager.add中的collection参数将组件加入集合。

每个集合中只允许每个名称有一个组件。添加第二个同名组件时,会自动移除(移出集合,若不再属于任何集合则彻底移除)第一个组件。这一语义在源码的add()remove_from_collection()中有明确实现(见 components_manager.py)。

from diffusers import ComponentSpec, ComponentsManager comp = ComponentsManager() # 为第一个 UNet 创建 ComponentSpec spec = ComponentSpec(name="unet", repo="stabilityai/stable-diffusion-xl-base-1.0", subfolder="unet", type_hint=AutoModel) # 为另一个 UNet 创建 ComponentSpec spec2 = ComponentSpec(name="unet", repo="RunDiffusion/Juggernaut-XL-v9", subfolder="unet", type_hint=AutoModel, variant="fp16") # 将两个 UNet 添加到同一个集合 - 第二个将替换第一个 comp.add("unet", spec.load(), collection="sdxl") comp.add("unet", spec2.load(), collection="sdxl")

这使得在基于节点的系统(例如 ComfyUI 式的可视化工作流引擎)中工作变得方便,因为你可以:

  • 使用collection标签标记所有从一个节点加载的模型;
  • 当新检查点以相同名称加载时自动替换模型;
  • 当节点被移除时批量删除集合中的所有模型(遍历该集合的组件 ID 调用remove即可)。

补充说明:一个组件可以同时属于多个集合(collectionsset存储),只有当一个组件不再属于任何集合时,remove_from_collection才会把它彻底移出管理器(见 components_manager.py)。


六、自动 CPU 卸载:跨管道的全局内存管理

ComponentsManager.enable_auto_cpu_offload是一种全局卸载策略,作用于所有已注册模型,与具体是哪个管道在使用它们无关。一旦启用,你添加或移除组件时都无需再手动操心设备放置(新组件会自动挂上同样的卸载钩子)。

comp.enable_auto_cpu_offload(device="cuda")

工作机制

从源码看(见 components_manager.py),该方法的算法如下:

  1. 所有模型开始时都在 CPU 上(CustomOffloadHook.init_hook会把模型移动到 CPU);
  2. 当某个模型的forward被调用时,CustomOffloadHook.pre_forward把它移动到执行设备(默认取PartialState().default_device);
  3. 如果设备内存不足,处于设备上的其他模型会被移回 CPU(通过AutoOffloadStrategy决策);
  4. 系统会尝试卸载能够释放足够内存的最小模型组合
  5. 模型会一直驻留在执行设备上,直到其他模型需要内存而被迫让位。

AutoOffloadStrategy 的决策细节

AutoOffloadStrategy(见 components_manager.py)的默认行为值得展开:

  • 构造函数接受memory_reserve_margin="3GB",即设备上预留的内存余量(防止推理中间激活值、梯度等把显存打爆),该字符串通过convert_file_size_to_int解析为字节数;
  • 决策时用model.get_memory_footprint()计算每个模型的内存占用,用torch.<device>.mem_get_info()查询设备空闲内存;
  • search_best_candidate会在所有驻留模型中穷举组合(itertools.combinations),寻找「总大小 ≥ 需释放内存、且总大小最小」的卸载组合;
  • 如果找不到满足条件的组合,会警告并卸载所有驻留模型

enable_auto_cpu_offload还有两个可调参数:

  • device:执行设备,默认用get_device()自动推断(MPS > GPU 0 > CPU,见CustomOffloadHook的文档说明);
  • memory_reserve_margin:内存余量,默认"3GB"
  • offload_strategy:自定义策略,任何签名为(hooks, model_id, model, execution_device) -> hooks的可调用对象,返回在下一个模型载入前需要卸载的驻留模型列表。

如果设备不支持mem_get_info(),会抛出NotImplementedError;未安装 accelerate 时会抛出ImportError(提示先pip install accelerate)。

自定义卸载策略

如果你不满足于默认的「最小组合」策略(例如希望大模型优先驻留、小模型优先被换出),可以自定义策略并通过enable_auto_cpu_offload(offload_strategy=...)传入;启用后还可以随时用set_offload_strategy热替换策略(仅限自动卸载开启状态下,见 components_manager.py)。

AutoOffloadStrategy源码注释中也留下了 TODO:未来计划支持设置maximum_total_models_size(设备上驻留模型的总大小上限),以便更精确地控制显存占用,而非仅靠内存余量间接控制。

卸载相关辅助 API

  • disable_auto_cpu_offload():对每个模型先offload()(移回 CPU)再移除钩子,清空设备缓存,重置全部卸载状态(见 components_manager.py);
  • 管道侧还有unload_components(names):把指定组件的属性置回None并释放内存;如果管道绑定了ComponentsManager,组件也会被同步移出管理器(见 modular_pipeline.py)。

测试佐证

仓库的测试 test_components_manager.py 从三个层面验证了上述行为:

  • 注册表行为(硬件无关):test_add_and_get_one验证add后组件在注册表中且get_one(name=...)/get_one(component_id=...)均能取回同一对象;test_add_same_component_twice_reuses_id验证同一对象重复添加会复用 ID 且注册表只有一条;test_remove验证移除后 ID 不再存在于注册表;
  • 元数据test_get_model_info_reports_size验证get_model_infosize_gb字段与model.get_memory_footprint() / 1024**3一致;
  • 自动卸载(需加速器):test_auto_offload_starts_with_all_components_on_cpu验证启用后所有组件初始都在 CPU;test_auto_offload_evicts_resident_model_under_memory_pressure验证内存充足时模型驻留设备、内存紧张时驻留模型被换回 CPU 为后来者腾位;test_auto_offload_keeps_models_resident_when_memory_is_ample验证内存充裕时两个模型同时驻留不互相驱逐。

这些测试清晰地展示了「所有模型初始在 CPU → forward 时按需上设备 → 内存不足时换出最合适的组合」的完整闭环。


七、典型工作流串联

把以上能力组合起来,一个典型的多管道 + 共享组件 + 自动卸载场景如下:

from diffusers import ModularPipeline, ComponentsManager # 1. 创建集中式组件管理器 comp = ComponentsManager() # 2. 第一个管道加载组件并归入 "base" 集合 pipe1 = ModularPipeline.from_pretrained( "YiYiXu/modular-demo-auto", components_manager=comp, collection="base", ) pipe1.load_components(dtype=torch.bfloat16) # 统一精度加载全部组件 # 3. 第二个管道复用同一批组件,仅归入不同集合 pipe2 = ModularPipeline.from_pretrained( "YiYiXu/modular-demo-auto", components_manager=comp, collection="inpaint", ) # 4. 识别缺失组件并从管理器补齐 missing = pipe2.null_component_names pipe2.update_components(**comp.get_components_by_names(names=missing)) # 5. 开启全局自动 CPU 卸载,从此无需手动管理设备 comp.enable_auto_cpu_offload(device="cuda", memory_reserve_margin="3GB") # 6. 切换检查点时,同一集合内同名组件自动替换 # comp.add("unet", new_unet, collection="inpaint") # 旧 unet 自动让位 # 7. 查看注册表全貌(表格形式展示设备、dtype、大小、load_id、集合) print(comp)

总结

ComponentsManager是 Modular Diffusers 中把「模型注册、元数据管理、去重、集合组织、自动卸载」统一起来的核心组件。通过本文你可以看到:

  • 添加组件:与ModularPipeline.from_pretrained/init_pipeline协作,组件延迟到load_components()时才真正加载并注册;
  • 检索组件get_one支持精确、通配、排除、OR 四种模式匹配,get_components_by_names可直接对接update_components
  • 重复检测ComponentSpec通过load_idrepo|subfolder|variant|revision)识别相同检查点,不同对象加载同一模型也能被警告;
  • 集合:每个集合内同名组件唯一,新组件自动替换旧组件,适合节点式工作流;
  • 自动卸载enable_auto_cpu_offload提供全局内存管理,默认AutoOffloadStrategy会卸载「释放足够内存的最小组合」,并支持自定义策略与memory_reserve_margin调优。

该功能目前仍是实验性的,接口可能随版本演进调整;建议在实际项目中以当前安装版本的源码为准进行验证。

【免费下载链接】diffusers🤗 Diffusers: State-of-the-art diffusion models for image, video, and audio generation in PyTorch.项目地址: https://gitcode.com/GitHub_Trending/di/diffusers

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/11 21:31:26

Openclaw浏览器自动化工具实战解析与应用

1. Openclaw&#xff08;龙虾&#xff09;浏览器自动化操作实现解析浏览器自动化工具正在成为现代办公和开发流程中的标配。Openclaw作为一款新兴的自动化解决方案&#xff0c;其独特的设计理念让它能够像龙虾钳子一样精准抓取和操作浏览器元素。我在实际项目中用它处理过表单自…

作者头像 李华
网站建设 2026/9/11 21:29:12

C++信奥刷题:P5133 tb148字符串处理与扫描线算法解析

1. 项目概述&#xff1a;信奥刷题与P5133 tb148题目解析信奥刷题是信息学竞赛&#xff08;OI&#xff09;选手提升编程能力的必经之路。今天我们要拆解的是《信息学奥赛一本通》中的P5133 tb148题目——"tb148的客人"。这道题看似简单&#xff0c;却蕴含了字符串处理…

作者头像 李华
网站建设 2026/9/11 21:27:29

沐曦C500+CubeStudio大模型全流程实操指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/11 21:27:12

JAVA毕设项目:基于SpringBoot的作业批改服务平台的搭建与实现 基于SpringBoot+Vue的作业提交与批改系统 (源码+文档,讲解、调试运行,定制等)

博主介绍&#xff1a;✌️码农一枚 &#xff0c;专注于大学生项目实战开发、讲解和毕业&#x1f6a2;文撰写修改等。全栈领域优质创作者&#xff0c;博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java、小程序技术领域和毕业项目实战 ✌️技术范围&#xff1a;&am…

作者头像 李华