包含关键字 typecho 的文章

Lab4AI大模型实验室是面向AI开发者、科研党与学习者打造的一站式AI实践平台,深度绑定高性能弹性算力,支持模型复现、训练、推理全流程,以按需计费、低价高效破解高端算力紧缺与成本高昂难题;同步Arxiv前沿论文并提供翻译、导读、分析服务,支持各类大模型一键复现与数据集微调,对接孵化资源助力科研成果转化;同时搭载多样化AI在线课程,实现理论学习与代码实操同步推进,全方位覆盖AI研发、科研创新与技能学习全场景需求。

大模型实验室官网链接: https://www.lab4ai.cn/arxiv?utm_source=sf_daily_paper

作者单位:北京大学计算机学院

研究背景

  1. 场景图是计算机视觉中建模物体及其语义、空间关系的结构化表示,广泛应用于机器人、自主导航、具身智能等3D环境理解任务。
  2. 现有多视图场景图方法(如MSG)在欧氏空间中通过对比学习与注意力关联学习嵌入,虽能实现较好的位置识别精度,但欧氏几何无法显式建模场景中场所-物体的层级蕴含关系,导致学习到的表示结构一致性不足。
  3. 真实场景具备天然层级结构:场所语义蕴含物体,物体间也存在层级语义关系,而欧氏嵌入难以高效表示这类层级与蕴含关系,常需更高维度或导致结构组织劣化。
  4. 双曲空间具备指数级容量增长特性,天然适配层级与蕴含关系建模,已在视觉表征、视觉-语言对齐等任务中验证有效性,但尚未被用于多视图场景图的层级结构学习。

研究目的

  1. 解决欧氏空间场景图嵌入无法有效捕获场所-物体层级蕴含关系的核心问题,提升场景图的结构一致性与质量。
  2. 提出适配场景图建模的双曲表示学习框架,在保持位置检索性能的同时,显著优化场景图层级结构指标。
  3. 设计显式的层级约束损失,强化场所与物体间的蕴含关系建模,兼容现有MSG构建流程。

本文核心贡献

image

  1. 提出双曲场景图(HSG)框架,首次将双曲几何引入多视图场景图学习,利用双曲空间天然特性编码场景层级关系。
  2. 设计双曲空间下的蕴含损失(Entailment Loss),显式约束场所-物体的层级蕴含结构,提升表示的结构一致性。
  3. 通过大量定量与定性实验验证,HSG在保持高位置检索精度的同时,大幅超越欧氏基线方法,在图级指标上实现显著提升。
  4. 提供兼容现有MSG pipeline的双曲嵌入映射方案,可无缝集成到多视图场景图构建流程中,具备良好扩展性。

研究方法

image

  1. 双曲空间基础:采用洛伦兹双曲面模型(Lorentz hyperboloid model),将欧氏嵌入通过指数映射映射到双曲空间,原点表示最抽象概念,越具体实体距离原点越远。
  2. 模型架构:沿用MSG整体架构,将L2归一化超球嵌入与余弦相似度替换为洛伦兹双曲面嵌入与负洛伦兹距离,新增蕴含损失。
  3. 核心技术

    • 双曲对比学习:基于负洛伦兹距离重构InfoNCE损失,分别优化场所级与物体级对比目标。
    • 蕴含损失:采用双曲蕴含锥,定义场所嵌入为锥中心,约束物体嵌入位于锥内,控制层级约束强度。
    • 总损失:总损失=场所对比损失+物体对比损失+λ×蕴含损失。
  4. 实验设置

    • 骨干网络:DINOv2-Base(最优),辅以ConvNeXt、ViT、ResNet等对比。
    • 数据集:ARKitScenes,4492个训练场景、200个测试场景。
    • 训练配置:AdamW优化器,学习率2e-6,曲率初始值80(可学习),损失权重比1:1:20。
    • 评价指标:Recall@1、PP IoU(场所-场所交并比)、PO IoU(场所-物体交并比)、Graph IoU(图交并比)。

研究结果

  1. 核心性能:HSG的Recall@1达98.39%,与最优欧氏基线相当;PP IoU为33.17%,Graph IoU为33.51%,超越最优AoMSG变体25.37%,提升8.14%。
  2. 维度与骨干影响:投影头维度1024时性能最优;DINOv2-Base作为骨干效果最佳,自监督预训练对双曲场景图学习至关重要。
  3. 消融实验

    • 移除蕴含损失:图级指标小幅下降,验证蕴含损失对层级结构的优化作用。
    • 固定曲率c=1:PP IoU骤降,层级结构失效,说明可学习曲率的必要性。
    • 替换为欧氏损失:图级指标大幅退化,证明双曲表示的核心价值。
  4. 定性结果:HSG的场所嵌入更靠近双曲原点(更抽象),物体嵌入远离原点,呈现清晰层级分布;欧氏基线无明显层级结构。
  5. 超参数鲁棒性:对蕴含锥孔径阈值、InfoNCE温度参数(最优0.1)具备良好鲁棒性。

总结与展望

  1. 研究总结:HSG通过双曲几何学习场景图嵌入,有效捕获场所-物体层级蕴含关系,在保持检索性能的同时大幅提升场景图结构质量,验证了双曲表示在结构化视觉推理中的有效性。
  2. 局限性

    • 双曲投影对投影器维度敏感,过大或过小均影响性能。
    • 性能高度依赖底层编码器质量。
    • 曲率优化方式较为简单,极端曲率易引发数值不稳定。
  3. 未来展望

    • 采用自适应或多阶段曲率优化,提升几何表达能力与训练稳定性。
    • 集成更强基础模型(如DINOv3)与开放词汇检测器(如GroundingDINO),提升泛化性。
    • 结合多模态线索与下游任务联合优化,拓展双曲场景图的应用场景。
    • 探索时序场景图框架,适配长期、大规模环境理解。

laminin521(LN521)在hPSC人多能干细胞培养成本、扩增效率与操作简化对比研究

本文摘要:人多能干细胞培养长期面临成本高、操作繁琐、稳定性差三大痛点。本文基于实验数据,系统解析laminin521(LN521)重组层粘连蛋白作为包被基质的核心优势,从单位面积成本、细胞扩增效率、单位细胞成本、培养便利性四个维度,与截短层粘连蛋白、EHS提取物、玻连蛋白进行对比,证明LN521可显著提升扩增效率、降低整体培养成本、简化操作流程,是hPSC基础研究与临床前培养的高性价比优选方案。


一、hPSC 培养的典型痛点与基质选择重要性

在人多能干细胞(hPSC)日常培养中,科研人员普遍面临三大核心问题:

  1. 成本高:基质、培养基消耗大,长期实验开销显著
  2. 周期长:细胞增殖慢,传代频繁,实验周期不可控
  3. 操作繁琐:需每日换液、手动剔除分化细胞,重复性差

包被基质直接决定细胞黏附、增殖、稳定性与整体成本,选择适配的基质是提升hPSC培养效率的关键(点击查看细胞外基质技术详情)。


二、Biolaminin 521(LN521)核心特点

laminin521是全长重组人源层粘连蛋白,为hPSC生理环境天然存在的细胞外基质,具备多重实验与产业化优势:

2.1 临床级合规稳定

无动物源、无异种成分,批次一致性高,满足标准化与可重复性要求。

2.2 高质量干细胞培养

支持均一、遗传稳定的 hPSC 生长,无需手动去除分化细胞。

2.3 高生理相关性

激活细胞天然信号通路,促进基因均匀表达,提升分化效率与成熟度。

2.4 易用灵活体系

兼容多种培养基;周末无需换液;支持单细胞传代,无需ROCKi;接种密度范围宽,易上手。


三、实验数据对比:LN521vs其他包被基质

本文数据基于 HS181(人胚胎干细胞)、iPSC3(诱导多能干细胞)两株细胞系,对比底物包括:

  • LN521(全长层粘连蛋白)
  • 截短型 LN 片段
  • EHS 肿瘤提取物
  • 玻连蛋白(Vitronectin)

3.1 单位面积培养成本更低

LN521使用浓度仅0.5 μg/cm²,基质消耗量少,单次传代单位面积总成本(基质+培养基)显著低于同类产品。

3.2 细胞扩增效率更高

  • 单次传代增殖倍数更高
  • 生长速率更快,在hESC与iPSC均表现一致优势
  • 相同时间内可获得更高总细胞产量

3.3 单位细胞成本显著更低

因增殖快、产量高,LN521的单次传代单位细胞成本远低于截短LN、EHS、玻连蛋白,整体性价比突出。

3.4 实验更省心:简化操作流程

  • 无需每日换液,**周末可停换液**
  • 单细胞传代,**不用 ROCKi**
  • 无需频繁挑取分化克隆,降低人为误差
  • 接种密度宽松,工艺容错率高

四、laminin521成本及hPSC培养效率数据展示

4.1 单位面积成本分析

hPSC培养的经济可行性首先体现在单位面积的培养成本上。实验数据显示(数据图1),在单次传代过程中,LN521 的单位面积成本(基质与培养基成本合计)低于同类但为截短/片段型层粘连蛋白。较低的基质用量使得单次细胞培养的耗材支出更加可控,为长期培养项目提供了经济基础。

不同包被基质下综合培养基传代的单位面积hPSC培养成本汇算
数据图1. LN521和其他包被基质结合培养基在单位面积下平均的单次传代hPSC培养成本汇算(包被基质(深色)和培养基(浅色斜线)的配制成本均根据市售产品定价计算。实验采用LN521涂层浓度为0.5微克/平方厘米,其他基质材料(截短型片段层粘连蛋白、EHS肿瘤提取物及玻连蛋白Vitronectin)均按制造商说明书使用。培养基用量为每平方厘米0.2毫升,并每日更换。传代次数取两个细胞系(HS181和iPSC3)的平均5-6次传代数据作为基准。)

4.2 细胞扩增效率

细胞产量与生长速度是评估培养体系的核心指标。在 HS181(人胚胎干细胞系)与 iPSC3(诱导多能干细胞系)的对比实验中(数据图2-4),Biolaminin全长LN521 相比于其他类型包被基质,如截短/片段型层粘连蛋白、EHS肿瘤提取物以及玻连蛋白Vitronectin,均获得了更高的细胞产量与更快的生长速度。

相较于其他包被基质BioLamina品牌LN521的每代次hPSC具更高增殖次数
数据图2. LN521和其他包被基质培养HS181和IPSC3单次传代hPSC扩增倍数统计(LN521相较于其他包被基质在不同细胞系中均表现较高的扩增倍数。HS181(人胚胎干细胞)和iPSC3(诱导多能干细胞)细胞系的单次传代中增殖次数基于5-6次传代的平均值计算,接种密度为50,000个细胞/平方厘米。)

相较于其他包被基质BioLamina品牌LN521对HS181细胞系培养具较高扩增速率
数据图3. LN521和其他包被基质培养HS181时细胞的生长速率统计(LN521相较于其他包被基质在HS181细胞系显示出更高的增殖速率。随时间的倍增累积计算为每次传代(5-6次传代)的HS181倍增(log2)的总和。)

相较于其他包被基质BioLamina品牌LN521对生长较慢的IPSC3培养也支持较高扩增速率
数据图4. LN521和其他包被基质培养HS181时细胞的生长速率统计(LN521相较于其他包被基质在生长较慢的IPS C3细胞系显示出更高的增殖速率。)

这种稳定的扩增表现意味着:

  • 相同时间内可获得更多细胞
  • 对生长特性不同的细胞系具有较好的适应性

4.3 单位细胞成本

将成本与产量结合分析,单位细胞成本更能反映培养体系的经济性。由于 LN521 培养体系下 hPSC 生长更快、周期内产量更高,其单次传代的单位细胞成本远低于其他基质(数据图5)。

相较于其他包被基质BioLamina品牌LN521对hPSC培养具每单位细胞省时与省钱成本优势
数据图5. 不同包被基质结合培养基在单次传代的单位细胞hPSC的培养成本汇算(LN521相较于其他包被基质的单次传代周期中基质(深色)和培养基(浅色斜线)单位细胞的成本较低。)

值得注意的是:

  • 结合细胞产量速率分析,基质与培养基的成本分配更均衡
  • 使用全长LN521无需通过过度增加培养基用量来增加产量
  • 接种密度为50,000细胞/cm²,无需特殊调整即可达到良好效果

以上hPSC培养良好表现,主要得益于全长的Biolaminin521(LN521)为hPSC天然环境存在的细胞外基质,在生物相关性较高的环境中,有助于细胞适应体外培养体系,获得较高细胞扩增倍数,获得较高的总细胞产量,进而分摊培养过程中基质和培养基的总体支出。


五、使用laminin521进行hPSC培养的常见问题FAQ

Q1:LN521 包被的使用浓度是多少?

A:标准使用浓度为0.5 μg/cm²,按厂家推荐即可,用量低、性价比高。

Q2:用LN521培养hPSC真的可以周末不换液吗?

A:可以。该体系支持周末无需换液,大幅降低操作负担,且不影响细胞状态。

Q3:使用LN521需要添加ROCKi吗?

A:不需要。支持单细胞直接传代,可省去 ROCKi,降低成本与操作步骤。

Q4:LN521 相比截短型层粘连蛋白优势在哪里?

A:LN521是全长层粘连蛋白,生理信号更完整,细胞黏附/增殖/稳定性更优,成本更低。

Q5:适用于哪些干细胞培养?

A:适用于hESC、iPSC等人多能干细胞的扩增、维持、分化及临床前研究。


六、总结

Biolaminin521(LN521)作为全长重组人源层粘连蛋白包被基质,在hPSC培养中实现了低成本、高扩增、易操作、高稳定的均衡优势,您可点击此处了解曼博生物提供的laminin521产品详情

它不仅能明显降低单位细胞培养成本,还能简化实验流程、提升结果可靠性,是人多能干细胞基础研究、药物筛选、临床前开发的理想选择,真正做到省时、省力、省钱。

在设备登录活动中,计算机账户与用户账户同样重要。如果用户的 Active Directory(AD)计算机账户被删除,该用户将无法登录其设备继续工作。这不仅会增加 IT 团队恢复账户的时间成本,还会导致员工无法正常工作的生产力损失。因此,找出是谁删除了计算机账户,对于管理员分析删除原因、防止类似问题再次发生具有重要意义。下面将介绍具体方法。

使用 PowerShell 查找删除计算机账户的用户步骤

在域控制器(Domain Controller,DC)上执行以下操作:

点击“开始”,搜索 Windows PowerShell,右键选择“以管理员身份运行”。

在控制台中输入以下脚本:

Get-EventLog -LogName Security | Where-Object {$_.EventID -eq 4743} | Select-Object -Property *

按下 Enter 键执行命令。

该脚本会显示已删除的计算机账户相关事件。在输出结果中,定位到 Message > Subject > Account Name,即可查看执行删除操作的用户名称及其安全标识符(SID)。

注意: 如果你是在工作站上执行查询,则需要使用以下脚本:

Get-EventLog -LogName Security -ComputerName <DC name> | Where-Object {$_.EventID -eq 4743} | Select-Object -Property *

image.png

其中 <DC name> 表示你要查询的域控制器名称。

image.png
通过这种方式,可以基于安全日志筛选出计算机账户被删除的事件,从而定位具体执行操作的用户。

需要注意的是,虽然通过原生审计功能可以查询对象删除事件,但当环境中存在大量计算机账户时,需要持续跟踪和分析每一次事件,这种方式会变得非常低效且难以维护。

使用 ManageEngine ADAudit Plus 查找删除计算机账户的用户

相比手动查询日志,使用专业审计工具可以大幅提升效率。通过 ManageEngine ADAudit Plus,管理员可以更快速、直观地获取删除操作的详细信息。

操作步骤如下:

打开 ADAudit Plus 控制台,并使用管理员账号登录。

导航至: Active Directory >计算机管理>最近删除的计算机,导航到报表。

管理员还可以根据不同条件对报告进行筛选,例如:

计算机名称

操作用户创建、删除或修改时间

目标计算机名称
image.png

通过这些筛选条件,可以有针对性地监控关键计算机账户,快速定位删除行为。

使用 ADAudit Plus 相比原生审计的优势

与 Active Directory 原生审计功能相比,ADAudit Plus 提供了更强大、更高效的审计能力。

首先,ADAudit Plus 能够对所有 Active Directory 变更进行持续审计和报告,确保形成完整、可靠的审计轨迹。管理员无需记住每种操作对应的事件 ID,也不需要手动筛选日志,大大简化了运维工作。

其次,平台支持基于机器学习的异常行为检测,可以针对异常操作设置实时告警,并自动触发响应机制,从而帮助企业及时发现和应对内部威胁。

此外,ADAudit Plus 还内置多种合规性报告模板,可帮助组织满足包括 SOX、HIPAA、GLBA、PCI-DSS、FISMA 和 GDPR 在内的多项法规要求,减少审计压力,提高合规效率。

早上想找个文件,才发现文件打不开了,其中有几个文件夹的文件图标都多了个黄色盒子,点击没有任何反应。可以排除电脑中毒(电脑有些文件夹的文件是正常的),在网上搜了好久,貌似是 Onedrive ,但是不确定。请大佬帮助指点一下怎么处理。

感谢感谢感谢鞠躬

【前言】

在生成式 AI 技术的推动下,企业级办公应用正经历着深刻的范式重构。作为业务数据处理与分析的基石,传统的电子表格如何跨越复杂的交互壁垒,真正迈入“自然语言驱动”的智能时代?面对高达数千个接口的表格 API 与大语言模型固有的“幻觉”挑战,开发者又该如何构建安全、稳定的 AI Agent 工程落地架构?

在近日落幕的“2026赋能开发者大会”产品技术分论坛上,葡萄城 SpreadJS / GcExcel 产品经理张明以《智慧表格:拥抱Web端对话式办公》为主题,全方位揭秘了 SpreadJS 结合 AI 的底层工程化实践,并深度剖析了行业内各大主流智能表格的技术演进路线。

一、 破局与重构:AI 赋予电子表格的新生命

传统电子表格在企业级业务应用中,长期面临着来自终端用户和前端开发者的双重痛点:

  • 终端用户困境:陡峭的学习曲线。传统的 Excel 交互繁杂且不够直观。对于非专业数据人员而言,诸如 VLOOKUP、INDEX、MATCH 等复杂函数和公式的学习成本极高,高级数据分析与报表美化更是存在难以跨越的技术门槛。
    在这里插入图片描述
  • 前端开发者困境:工程化落地的鸿沟。面对 SpreadJS 庞大的 API 库(包含数千个接口),直接结合大语言模型极易触发“模型幻觉”。此外,如何维持表格实例与 AI 之间的状态同步,也是工程落地中的核心难题。
    在这里插入图片描述

为了打破“人找工具”的壁垒,实现从传统操作向“自然语言驱动”的转变,葡萄城 2026 客户峰会-表格分论坛展示了 SpreadJS 结合 AI 后的三大核心业务场景(Demo):

1.具备业务 Know-how 的数据理解:AI 能够智能分析导入的报表结构。例如在处理资产负债表时,AI 不仅能自动生成汇总计算公式,还能执行如 Validations = 资产总计 - (负债合计 + 所有者权益合计) 的专业财务校验逻辑,确保底层计算逻辑的一致性与准确性。
在这里插入图片描述

2.自然语言驱动的高级美化:用户仅需输入简单的格式指令,AI 即可自动应用复杂的商务风格设计规范。系统能够自动执行包括设置主标题为海军蓝粗体、年份表头中蓝底色、明细数据白浅蓝斑马纹、层级缩进以及千位分隔符等一系列繁琐操作,瞬间完成报表的专业化重构。
在这里插入图片描述

3.一键可视化及数据洞察:根据当前选区的数据,AI 可快速提炼出资产规模、流动性、资本结构等关键业务洞察,并自动生成结构清晰的图表(如“2025 年财务健康程度分析”视图),辅助管理层进行快速决策。
在这里插入图片描述

这些能力的落地,使得智能表格能够广泛应用于智能财务风控、自动化审计以及管理层数据驾驶舱等垂直场景。

二、 稳若磐石:工程化架构深度揭秘

要将 AI 可靠地接入极其复杂的表格系统中,底层的架构设计至关重要。本次分论坛公开了 SpreadJS AI Agent 的六层核心架构,并详细拆解了其中的三大技术创新点:

1.全局架构总览

系统自上而下划分为六层:表现层 (Presentation) 负责 UI 与用户交互;状态层 (State) 解决多端同步;业务逻辑层 (Business Logic) 处理对话与分发;服务与 AI 层 (Service / AI) 进行路由与幻觉治理;工具层 (Tool) 定义具体操作;数据与底层操作层 (Data) 负责 API 执行与外部通信。
在这里插入图片描述

2.核心技术点解析

  • 打造专属的工具库 (Tool Registry)
  • 鉴于 SpreadJS 功能跨度极大,且大模型存在上下文限制(一次性暴露过多工具极易导致“认知过载”),系统放弃了单一通用的工具设计。取而代之的是原子化执行策略:将复杂操作拆解为 read_rangeswrite_dataset_cell 等基础原子工具,再结合网关工具和特定行业工具,以此保障极高的执行成功率和精细化的安全合规管控。
    在这里插入图片描述

在这里插入图片描述

  • 基于 SpreadContext 的全局状态同步

为了解决 AI 对话框、Spread 设计器界面、后台工具执行流这三方访问同一工作簿时的冲突问题,引入了 SpreadContext 作为全局状态中心。它安全、高效地管理着 Spread 工作簿实例,确保数据读写的绝对一致性。
在这里插入图片描述

  • 渐进式 API 披露 (ModuleTracker)
  • 这是治理大模型“工具选择幻觉”的核心机制。ModuleTracker 依托于基于有限状态机 (FSM) 的动态路由系统:

    • 默认模式:仅向 LLM 暴露约 30 个最常用的基础数据读写工具和网关工具。
    • 网关触发:当用户下达特定指令(如“创建图表”),LLM 调用 manage_chart 网关工具,触发状态机切换。
    • 进入模块:系统进入专属的 Chart 模块,此时才向 LLM 暴露 add_chart 等深度操作 API。
    • 任务闭环:操作完成后,自动调用 exit_module 退出并重置回默认状态,彻底隔绝无关上下文的干扰。

在这里插入图片描述

在这里插入图片描述

三、 行业视野:对话式智能表格的三大主流实现路线

立足于当前的行业生态,实现对话式智能表格主要有三条主流技术路线,各自具备不同的技术特征与适用场景:

路线模式代表案例原理简述核心优势局限性
Tool-based (后端解析模式)RampLLM 仅输出结构化 JSON,后端进行参数验证 (Schema Validation) 后执行具体操作。具备极高的系统安全性,执行结果高度可预测,且审计链路清晰。灵活性较低,受限于预设的工具集合,难以组合出不可预知的复杂操作。
Code Gen (前端代码生成模式)Shortcut将全量 API 文档作为上下文输入,动态生成 JS 源码块,并通过前端浏览器原生引擎执行。极致灵活,无预设代码限速,可原生支持复杂图表与深度数据验证。安全管控挑战极大,极度依赖顶级大模型的海量 Token 消耗与自我纠错能力。
Python Sandbox (后端沙箱模式)Sourcetable利用云端隔离沙箱运行 Python/Pandas 代码,随后通过 WebSockets 将 DataFrames 渲染结果传输至前端。完美契合数据科学工作流,能够突破 Token 限制处理 PB 级海量数据。系统架构沉重复杂,AI 对前端表格 API 的控制较弱,难以实现单元格级别的精细化格式还原。

(注:业内如扣子(Coze)、Genspark、Skywork 等平台也在积极探索 AI 与办公场景的结合,但底层核心逻辑多跳脱不出上述三种范式。)

在这里插入图片描述

四、 殊途同归:为什么 SpreadJS 是无法替代的底座?

无论企业选择上述哪种工程化架构来构建智能表格系统,最终都需要一个极其强大的前端电子表格组件作为承载底座。SpreadJS 之所以成为通向成功的最优解,原因在于以下三项核心能力:

  1. 高度兼容任意 AI 架构:无论是基于后端的 Tool Calling 验证执行流、前端动态生成的代码执行沙箱,还是前文提到的渐进式状态机,SpreadJS 开放且灵活的 API 体系均能提供完美支撑。
  2. 全面覆盖复杂业务场景的 API 深度:企业级需求远不止于简单的“数据填充”。面对 AI 生成的复杂逻辑(如透视表 PivotTable、条件格式 ConditionFormat、高级图表 Charts 等),SpreadJS 具备无损的、企业级的渲染与还原能力。
  3. 卓越的运行态控制与快照提取:SpreadJS 提供的原生 Headless 能力以及结构化数据导出功能,是支撑 AI 获取精准上下文 (Context) 并进行多模态理解的底层基石。
    在这里插入图片描述
    为了进一步降低企业接入门槛,赋能开发者群体,SpreadJS AI Agent Framework 现已正式开源。开发者可通过访问 Gitee 官方仓库 (gitee.com/grapecity/spreadjs-ai-agent) 获取完整源码,深入体验这套融合了结构化 Tool Calling、模块化状态机与受保护沙箱的先进框架体系,共同迈向对话式办公的新纪元。
    在这里插入图片描述

核心功能模块:

商品展示:展示护航代练套餐,如红物资、吞天包等,提供详细的服务描述和价格信息。

快速下单:简化下单流程,支持一键提交,实现“秒上号”核心卖点。

订单管理:用户可查看订单进度,商家可接单处理订单,支持订单状态跟踪、支付、售后等功能。

基础用户体系:支持手机号登录、微信授权登录等多种登录方式,提供个人中心页面,展示用户信息、订单记录等。

抢单与派单双模式:支持打手主动抢单(提升积极性)或客服手动派单(确保高价值订单分配),提高派单效率和打手日均接单量。

运营赋能:从搭建到盈利的闭环支持

快速部署:提供详细图文教程与环境配置指南(Nginx+PHP+MySQL5.6),技术小白也可在1小时内完成服务器搭建与小程序上线。

多俱乐部管理:支持无限创建独立俱乐部站点,总后台可统筹各俱乐部数据,子后台实现本地化运营(如定制打手招募规则、玩家会员体系),适合团队跨区复制盈利模式。

盈利模式多元:通过订单抽佣、打手入驻费、会员增值服务(如优先抢单、专属客服)、广告位出租等方式实现变现,源码内置财务结算模块,自动统计收益与提现记录。

在新零售浪潮席卷全球的今天,传统商超正加速向数字化、智能化转型。作为连接商品、消费者与后台管理的核心枢纽,一套高效、稳定、易用的收银系统已成为现代商超运营不可或缺的基础设施。OctShop不仅是一款广受认可的开源B2B2C电商系统,其延伸打造的商超收银系统模块,正以软硬结合、前后端协同、线上线下融合的优势,为中小型超市、便利店、生鲜店及社区零售门店提供一体化智能收银解决方案。

图片

OctShop商超收银系统源码详细介绍: https://pc.opencodetiger.com

OctShop商超收银系统并非简单的支付终端,而是一个集商品管理、库存同步、会员营销、销售分析与多支付方式于一体的全链路零售操作系统。它深度集成于OctShop整体电商生态中,既可独立部署用于线下门店,也能与线上商城无缝打通,真正实现“一店双营”——线下收银、线上下单、库存共享、会员互通。系统采用轻量化设计,支持Windows、Linux及国产操作系统,兼容主流POS机、扫码枪、小票打印机、钱箱、电子秤等外设设备。商家只需普通电脑或平板搭配基础硬件,即可快速搭建专业收银台,大幅降低初期投入成本。同时,得益于OctShop开源架构,系统支持高度定制化,可根据不同业态(如生鲜、烟酒、日百)灵活配置商品分类、促销规则与结算流程。在核心功能上,OctShop收银系统突出“快、准、稳”三大特点:快:扫码即出商品信息,支持批量扫码、组合商品、快速退货,单笔交易平均处理时间低于5秒;准:实时同步云端商品库与价格策略,杜绝价签不符或库存超卖;支持按重量、按件数、按规格等多种计价方式,尤其适合生鲜场景;稳:本地缓存+云端备份双机制,即使网络中断仍可离线收银,恢复后自动同步数据,保障营业连续性。更值得称道的是其强大的会员与营销能力。顾客在收银时可即时注册会员、积分累积、使用优惠券或参与满减活动。系统自动记录消费行为,生成用户画像,为后续精准营销提供数据支撑。商家可通过后台一键发放电子优惠券、设置生日特权、开展储值返现等活动,有效提升复购率与客户黏性。

图片

在支付方面,OctShop收银系统全面支持微信、支付宝、银联云闪付、数字人民币、会员余额、现金、银行卡等多种支付方式,并可自定义组合支付(如“微信,积分”)。所有交易流水实时入账,对账清晰,财务人员可随时导出日报、月报,极大简化财务管理流程。此外,系统内置智能库存预警与采购建议功能。当某商品库存低于设定阈值时,自动提醒补货;结合历史销售数据,还能预测热销品趋势,辅助采购决策。对于拥有多门店的连锁商超,OctShop支持总部统一管理商品、价格与促销策略,各门店独立运营但数据集中可视,实现“千店千面,一盘棋管”。安全性方面,OctShop收银系统遵循金融级数据加密标准,操作日志全程留痕,权限分级管控(如收银员仅能结账,店长可查看报表),有效防范内部操作风险。所有敏感信息均不存储于本地设备,确保用户隐私与商业数据安全。值得一提的是,作为开源项目,OctShop商超收银系统无授权费、无年服务费,企业可自主部署、自由修改,避免被SaaS厂商“锁定”。活跃的开发者社区和详尽的技术文档,也为后续维护与功能扩展提供了坚实保障。

图片

总之,OctShop商超收银系统不仅是收银工具,更是商超数字化转型的入口。它以开源为基、以体验为先、以效率为核心,帮助传统零售门店降本增效、提升服务、连接线上,真正迈向“智慧零售”新阶段。在竞争日益激烈的零售市场中,选择OctShop,就是选择一个开放、灵活、可持续进化的未来。

亚马逊云科技宣布 DevOps Agent 正式可用,这是一款由生成式 AI 驱动的智能助手,旨在帮助开发者和运维人员排查问题、分析部署,并在 AWS 环境中自动化执行运维任务。

该服务在 2025 年的 re:Invent 大会上预览发布,基于 Amazon Bedrock AgentCore 构建。DevOps Agent 通过学习应用关联关系并集成可观测性工具、运行手册、代码仓库和 CI/CD 管道来分析事件。该智能体可将遥测数据、代码和部署数据关联起来,自主完成问题分类与排查,加快故障解决,并从历史事件中识别规律、给出优化建议,助力防范后续故障。AWS 高级解决方案架构师 Madhu Balaji 在宣布正式可用时表示

SRE 在凌晨 2 点收到告警时往往需要手动整合多个来源的遥测数据,梳理跨服务依赖关系并进行问题假设,这一过程通常耗时数小时。随着系统复杂度的增加,对 AI 驱动的运维队友——SRE 智能体的需求变得日益迫切。

正式可用版本的主要改进包括:支持对 Azure 及本地环境中的应用进行排查、支持通过自定义智能体 Skill 扩展能力,同时新增自定义图表与报告功能。Balaji 补充道:

DevOps Agent 并非一个被动的问答工具,而是一个能自主行动的运维助手。当事件通过 CloudWatch 告警、PagerDuty 警报、Dynatrace 问题、ServiceNow 工单或通过 WebHook 配置的其他任意事件源触发时,该智能体无需人工干预即可立即启动排查工作。

在另一篇文章中,Janardhan Molumuri、Bill Fine、Joe Alioto 和 Tipu Qureshi 以一个无服务器 URL 短链应用为例,解释了如何利用智能体式 AI 通过 DevOps Agent 实现自主事件响应。他们写道:

借助 MCP 的可扩展性以及与 CloudWatch、Datadog、Dynatrace、New Relic、Splunk、Grafana、GitHub、GitLab 和 Azure DevOps 的内置集成,智能体可以从团队运维数据所在的任意位置获取监测信号。

来源:AWS 博客

亚马逊云科技表示,DevOps 团队通常会借助接入日志与监控系统的 AI 编码工具开展事件排查,但这类工具缺乏在大规模复杂生产环境中管理所需上下文信息和运维管控能力。Agentic Hamburg 联合创始人 Sebastian Korfmann 写道

早期数据表现亮眼:预览阶段的平均故障恢复时间(MTTR)最高降低 75%,根因分析准确率达到 94%,可与 Datadog、Grafana、Splunk、PagerDuty、ServiceNow 等平台集成。

The Duckbill Group 首席云经济学家 Corey Quinn 评论道

你花钱让 AI 去做凌晨两点值班工程师的活儿,只不过它事后不会在 Slack 上阴阳怪气地 @ 整个团队。平均故障恢复时间从小时级压缩到分钟级,而账单则从按分钟计费变成了按小时计费。

Reddit 上的一个热门讨论帖中,不少开发者对其缺乏问责机制提出了质疑,用户 The_Flexing_Dude 问道:

这和上个月搞崩生产环境的是同一个吗?

随着该服务正式可用,不再提供免费使用,定价基于代理执行运维任务的累计时长,按秒计费。AWS Support 客户可根据上月支持服务支出,获得每月对应的 DevOps Agent 使用额度,可使用额度的百分比随支持级别而定。目前该服务已在六个区域上线,包括弗吉尼亚北部、爱尔兰和法兰克福。

在另一项公告中,亚马逊云科技宣布 Security Agent 按需渗透测试功能正式可用。这款 AI 驱动的智能体可持续分析应用设计、代码和运行时行为,自动执行按需渗透测试并识别可被利用的安全漏洞。

【声明:本文由 InfoQ 翻译,未经许可禁止转载。】

查看英文原文:https://www.infoq.com/news/2026/04/aws-devops-agent-ga/

在日常办公和文档管理中,我们经常需要将多个独立的 PDF 文件整合成一个完整的文档。无论是将分散的章节合并成完整的报告,还是将多份合同文件整理为单一档案,PDF 合并操作都是一项非常实用的技能。

本文将介绍如何使用 Python 和 Spire.PDF 库来合并多个 PDF 文件,包括简单的顺序合并、选择性页面导入以及基于流的合并等多种方法,帮助您高效地完成文档整合任务。

为什么需要合并 PDF 文件?

合并 PDF 文件在实际工作中有着广泛的应用场景:

  • 文档整合:将分散的章节、附录或补充材料合并成完整的报告或手册
  • 档案管理:将相关的多份文件(如合同、附件、补充协议)整理为单一档案
  • 简化分享:将多个小文件合并为一个,便于通过邮件发送或在线分享
  • 批量处理:自动化合并大量 PDF 文件,提高工作效率
  • 保持格式:与转换为其他格式再合并相比,直接合并 PDF 可以保持原有的排版和样式

通过 Python 自动化这一过程,可以快速处理大量文件,避免手动操作的繁琐和出错风险。

环境准备

首先,需要安装 Spire.PDF for Python 库。可以通过 pip 命令轻松完成安装:

pip install Spire.PDF

安装完成后,即可在 Python 脚本中导入该库并使用其提供的文档合并功能。

基础合并:通过选择页面构建新 PDF

使用 InsertPage 和 InsertPageRange 方法

除了简单的全文合并,Spire.PDF 还允许我们创建一个全新的 PDF 文档,并从现有的多个 PDF 文件中挑选特定页面或页面范围进行组合。这种方法非常适合需要对页面顺序进行重组或仅提取部分内容的场景。

以下代码演示了如何从三个不同的 PDF 文件中提取特定页面,并将它们整合到一个新的文档中:

from spire.pdf import *
from spire.pdf.common import *

# 定义要处理的 PDF 文件路径
file1 = "示例1.pdf"
file2 = "示例2.pdf"
file3 = "示例3.pdf"
files = [file1, file2, file3]

# 加载所有 PDF 文件
pdfs = []
for file in files:
    # 实例化 PdfDocument 对象并加载文件
    doc = PdfDocument()
    doc.LoadFromFile(file)
    pdfs.append(doc)

# 创建一个新的空 PDF 对象(用于存放合并后的页面)
newPdf = PdfDocument()

# 策略 1:插入单个页面 (InsertPage)
# 将第一个文档的第 1 页(索引 0)插入新文档
newPdf.InsertPage(pdfs[0], 0)
# 将第二个文档的第 2 页(索引 1)插入新文档
newPdf.InsertPage(pdfs[1], 1)

# 策略 2:批量插入页面范围 (InsertPageRange)
# 将第三个文档的第 1 页到第 2 页(索引 0 到 1)一次性插入新文档
newPdf.InsertPageRange(pdfs[2], 0, 1)

# 保存合并后的新 PDF 文档
newPdf.SaveToFile("output/复制页面合并PDF.pdf")

# 关闭资源
newPdf.Close()
for pdf in pdfs:
    pdf.Close()

这段代码展示了精准控制页面合并的两种核心技术:

  1. InsertPage 方法:用于从源文档中提取单个特定页面。它接受源文档对象和页面索引作为参数。在示例中,我们分别从前两个文档中各取一页放入新文档。
  2. InsertPageRange 方法:用于批量提取页面范围。它接受三个参数:源文档对象、起始页面索引和结束页面索引。相比多次调用 InsertPage,这种方法在处理连续章节合并时效率更高。

通过这种方式,你可以打破原有的文档结构,像积木一样自由组合来自不同来源的页面,生成一个完全定制化的新 PDF 文件。

高级合并:使用流进行合并

使用 PdfMerger.MergeByStream 方法

除了基于文档对象的合并方式,Spire.PDF 还提供了基于流的合并功能。这种方法特别适合处理来自网络或内存中的 PDF 数据,无需先将数据保存到磁盘文件。

以下示例展示了如何通过流的方式合并多个 PDF 文件:

from spire.pdf.common import *
from spire.pdf import *

# 定义输入文件路径和输出流
inputFile1 = "./Demos/Data/MergePdfsTemplate_1.pdf"
inputFile2 = "./Demos/Data/MergePdfsTemplate_2.pdf"
inputFile3 = "./Demos/Data/MergePdfsTemplate_3.pdf"
outputFile = Stream("MergeFilesByStream.pdf")

# 创建 PDF 文档流
stream1 = Stream(inputFile1)
stream2 = Stream(inputFile2)
stream3 = Stream(inputFile3)

# 将所有流放入列表
streams = [stream1, stream2, stream3]

# 创建合并选项
mergeOp = MergerOptions()

# 通过流合并 PDF 文件
PdfMerger.MergeByStream(streams, outputFile, mergeOp)

这种基于流的合并方法有以下优势:

  • 内存效率:可以直接处理内存中的数据,减少磁盘 I/O 操作
  • 网络友好:适合处理从网络下载的 PDF 数据,无需先保存到本地
  • 简洁高效:一行代码即可完成多个文件的合并,代码更加简洁

MergerOptions 类允许您配置合并过程中的各种选项,例如是否保留书签、如何处理元数据等。虽然本示例使用了默认设置,但在实际应用中可以根据需要进行自定义配置。

实际应用

PDF 合并功能在实际工作中有广泛的应用场景:

批量合并文件夹中的所有 PDF

当需要将某个文件夹中的所有 PDF 文件按名称顺序合并时,可以编写批处理函数来自动化这一过程。以下是一个实用的批量合并示例:

from spire.pdf.common import *
from spire.pdf import *
import os
import glob

def MergePdfFolder(input_folder: str, output_file: str):
    """将文件夹中的所有 PDF 文件按名称顺序合并"""
    
    # 获取文件夹中所有的 PDF 文件并按名称排序
    pdf_files = sorted(glob.glob(os.path.join(input_folder, "*.pdf")))
    
    if not pdf_files:
        print("未找到 PDF 文件")
        return
    
    print(f"找到 {len(pdf_files)} 个 PDF 文件,开始合并...")
    
    # 加载第一个 PDF 文档作为基础文档
    main_doc = PdfDocument()
    main_doc.LoadFromFile(pdf_files[0])
    print(f"已加载基础文档: {os.path.basename(pdf_files[0])}")
    
    # 依次将其他文档追加到基础文档
    for i in range(1, len(pdf_files)):
        temp_doc = PdfDocument()
        temp_doc.LoadFromFile(pdf_files[i])
        main_doc.AppendPage(temp_doc)
        temp_doc.Close()
        print(f"已合并: {os.path.basename(pdf_files[i])}")
    
    # 保存合并后的文件
    main_doc.SaveToFile(output_file)
    main_doc.Close()
    
    print(f"\n合并完成!输出文件: {output_file}")
    print(f"总共合并了 {len(pdf_files)} 个文件")

# 使用示例
input_folder = "./PDF文档"
output_file = "合并结果.pdf"
MergePdfFolder(input_folder, output_file)

这个函数会自动扫描指定文件夹中的所有 PDF 文件,按文件名排序后依次合并,非常适合处理章节化的文档或系列报告。

生成综合报告

企业可以将各部门提交的独立报告合并成一份综合年度报告,保持整体结构的同时方便统一分发和归档。

合同文件整理

法务部门可以将主合同、附件、补充协议等相关文件合并为一个完整的合同包,便于管理和查阅。

电子书制作

将多个章节的 PDF 文件合并成完整的电子书,为读者提供连续的阅读体验。

实用技巧

在进行 PDF 合并时,以下技巧可以帮助获得更好的结果:

  • 文件顺序:在合并前确保文件按照期望的顺序排列,可以通过文件名编号来控制顺序
  • 页面方向:如果合并的文档有不同的页面方向(横向/纵向),合并后会保持各自的原始方向
  • 书签处理:合并后的文档可能会保留各原文档的书签,注意检查书签的层级结构是否合理
  • 文件大小:合并大量大文件时注意内存使用情况,考虑分批处理
  • 验证结果:合并完成后务必打开结果文件进行检查,确保所有页面都正确包含且顺序无误

总结

通过本文的介绍,我们学习了使用 Python 和 Spire.PDF 库合并 PDF 文件的多种方法:

  • 使用 AppendPage 方法将整个文档追加到目标文档末尾
  • 使用 InsertPage 方法选择性地将特定页面插入到目标文档
  • 使用 PdfMerger.MergeByStream 方法通过流进行高效合并
  • 实现批量合并功能处理文件夹中的多个 PDF 文件

这些技术为 PDF 文档的整合和管理提供了强大的工具。掌握这些技能后,您将能够高效地合并多个 PDF 文件,将分散的文档资源整合为统一的完整文档,显著提升工作效率和文档管理的专业性。

我的梯子在手机上能用,昨天在单位电脑也能用,今天早上来,电脑重启了,就不能用了。这个 ai 时代不能用梯子简直是要了我的老命sobbing。我原本用的是 clash verge 和 mihomo party,这俩现在在单位电脑都不能用,家里的电脑用的是很老版本的 clash 也是能用的,证明我的订阅地址没问题。我切换了手机网连公司电脑,也是不能用。大佬们帮我推荐下 pc 的梯子客户端,要不用翻墙就能下载的。。。。

很多人以为IP地址查询能直接定位到家庭门牌号,也有人觉得它什么都查不到。实际上,IP查询有明确的能力边界:它能定位到城市级或区县级,能识别网络类型(住宅宽带/数据中心/移动网络),但无法精准锁定具体街道或家庭地址。 理解这个边界,才能科学地保护隐私。本文通过三步实操,带你用IP查询工具自查暴露风险,并给出可落地的网络出口配置方案。

一、IP查询到底能查到什么?实测数据说话

IP归属地查询的本质,是将IP地址映射到运营商注册的地理位置。我们以一台连接家庭宽带的电脑为例,用IP查询工具进行一次实际查询(以IP数据云在线页面为例),使用正规IP查询工具查询IP归属地,是单向、无记录的查询过程。工具仅根据IP返回公开的地理信息,不会记录查询者的身份、不会存储查询历史,更不会暴露用户自己的隐私。返回的信息如下:

字段示例精度说明是否可查
国家中国准确率 >99.9%✅ 能查
省份广东省准确率 >99%✅ 能查
城市深圳市准确率 96-99%✅ 能查
区县南山区商业增强库支持✅ 能查(部分工具)
具体街道/门牌号无法获取❌ 不能查
实时位置轨迹无法获取❌ 不能查
网络类型住宅宽带可区分机房/代理✅ 能查

实操第一步:自查你的IP暴露信息

打开任意IP查询网站(如ipdatacloud.com),页面会自动显示你当前公网IP的归属地、运营商和网络类型。记录下显示的城市和网络类型,用于下一步评估。

二、IP查询的边界:哪些信息是查不到的?

很多人担心“别人查我的IP就能知道我住哪”,实际上这是不可能的。IP地址由运营商分配,且会动态变化。运营商对外公开的注册信息通常只到城市级。

IP查询的三大能力边界

  1. 无法定位到具体街道或门牌号:除非运营商主动泄露内部数据,否则任何公开IP库都做不到。
  2. 无法实时追踪设备:IP查询只是查数据库,不是实时探测。
  3. 无法绕过NAT识别具体设备:同一个家庭WiFi下的多台设备,对外显示同一个公网IP。

实操第二步:对比IP归属地与真实位置

  1. 用手机连接家庭WiFi,查询IP归属地城市。
  2. 断开WiFi,改用4G/5G流量再次查询。
  3. 对比两次结果:不同网络出口显示不同城市,说明出口IP已天然隔离——这是配置网络出口的基础

三、如何用IP查询工具自查隐私暴露风险?

如果你担心自己的真实IP被网站或应用获取,可以主动用IP查询工具自查,了解当前网络出口暴露了哪些信息。

实操第三步:三步完成风险自查

步骤操作判断标准
1. 查当前IP访问IP查询网站,记录归属地城市和网络类型若网络类型显示“数据中心”或“hosting”,风险较高
2. 对比常用地分别用家庭宽带、手机流量、公司网络查询,对比三次显示的城市若不同,则网络出口已天然隔离
3. 评估暴露面思考:是否需要在所有网站上使用同一个IP?对于社交、购物等账号,固定IP有利于风控

四、安全配置网络出口:不依赖敏感工具也能降低暴露风险

理解IP查询的边界后,保护隐私的核心不是“隐藏IP”,而是合理配置网络出口。以下是几种不依赖敏感工具的安全方案,附带具体操作步骤:

方案具体操作隐私效果
切换WiFi/移动热点关闭家庭WiFi,打开手机热点连接电脑出口IP变为移动基站,难以关联个人身份
重启路由器获取新IP断开路由器电源30秒后重开,部分运营商会分配新IP中断IP与行为的长期关联
使用公共WiFi前往商场、咖啡馆连接免费WiFi出口IP为公共IP,多人共享,无法追踪个人
企业专线/远程办公通过公司远程接入系统(注意:这里指企业合规,用于办公)出口IP为公司公网IP,与个人宽带隔离

实操建议(可立即执行)

对于普通用户,最简单的隐私保护方法是定期重启路由器获取新IP,或者在不同场景下使用不同的网络出口(如家用宽带用来看视频,手机热点用来登录敏感账号)。不需要复杂的技术,就能有效避免IP被长期关联。

五、总结

IP查询工具的价值在于帮助用户了解自己的网络暴露面,而不是制造恐慌。它能查到城市级位置和网络类型,但无法定位到具体住址。通过三步实操(查IP→对比出口→评估风险),用户可以评估当前隐私暴露程度,并通过简单的网络出口切换(重启路由器、切换WiFi/热点)来降低长期被追踪的可能。

在查询IP是也要选择专业工具,合理保护隐私,IP数据云提供城市级定位和网络类型识别能力,能帮助用户快速完成上述自查。理解IP查询的边界,配合合理的网络出口配置,就能在不依赖敏感工具的前提下,有效保护个人隐私。

aicoding.sh 这个服务快一年了,聊聊我们现在的状态。

目前平台注册用户数万,企业客户超过 100
家,日均调用量稳定在百万级。能跑到这个规模,核心就一件事——把稳定性做到位。我们
自建了国内多节点网络架构,直连无需任何代理配置,P99 延迟控制在可接受范围内。

价格方面完全透明,按 Claude 官方价的 0.19 折、0.29 折、0.49
折三档消耗余额,按量计费,后台实时可查每一笔调用的 token
用量和费用明细。没有套餐绑定,没有隐藏收费,用多少扣多少。

Claude Code
用户可以直接接入,标准化的配置流程,几行环境变量搞定。目前平台上大量开发者在用
Claude Code 做日常开发,高峰期并发也没出过稳定性问题。

支持国内支付体系,企业客户可以走对公转账开票。

加入用户社群可领 10
美元体验额度,实际跑一跑比什么介绍都直观: https://aicoding.sh

如果您的软件面向Windows生态分发、涉及敏感数据或驱动开发,EV代码签名证书无疑是最优之选。选择权威CA机构(如JoySSL),完成严谨的身份核验,让您的软件从发布第一刻起便赢得系统与用户的信任。

一、什么是EV代码签名证书?

EV(Extended Validation,扩展验证)代码签名证书,是遵循最严格国际验证标准的数字证书。它不仅仅验证开发者对代码的所有权,更通过严格的线下企业身份核实流程,确认申请者是一个合法、真实存在的实体机构。

与普通代码签名证书(如OV组织验证证书)相比,EV证书的审核堪称“数字身份的政审”。CA机构会要求企业提交营业执照、法人身份证明、对公账户证明、办公场地证明等全套材料,并通过工商数据库、银行渠道交叉验证,甚至直接拨打企业公开电话与法定代表人确认申请意愿。审核周期通常为3至7个工作日。这种穿透式审核从源头杜绝了虚假身份申请证书的可能,而普通证书曾出现过“空壳公司申请证书签署恶意软件”的案例,印证了身份核验差异带来的安全鸿沟。

二、为何EV证书是开发者的首选?

1. 硬件级私钥保护:杜绝私钥泄露

EV证书最核心的安全机制在于私钥的强制硬件存储。其私钥必须存储在符合FIPS 140-2标准的硬件安全模块(HSM)或USB加密狗中,与开发者设备物理绑定,每次签名需插入加密狗并输入独立密码,形成“硬件+密码”的双重防护。普通证书的私钥若存储在本地硬盘,可能因电脑中毒导致泄露;而EV证书的私钥无法被导出或复制,从根源上杜绝了签名滥用风险。

2. 系统级即时信任:突破SmartScreen拦截

对Windows平台开发者而言,“Windows SmartScreen已阻止启动此未验证的应用”是影响软件分发与用户信任的最大障碍。普通代码签名证书需从零开始积累信誉——只有当软件被足够多用户下载且未触发安全警报时,评分才会逐步提升,这一过程可能持续数周甚至数月。而EV证书凭借CA机构的穿透式审核,在签发时就为软件注入“初始信誉分”,新软件发布即可绕过拦截。测试数据显示,使用EV证书后首次发布的软件拦截率可降至8%,安装完成率提升至76%。

此外,EV证书具备“信誉继承”能力。当企业发布新版本软件时,SmartScreen会自动识别签名信息并关联历史信誉,某ERP厂商使用EV证书后,新版本发布时的初始信誉分达到历史版本的80%,较更换证书前提升65%。

3. 强制场景适配:驱动开发与合规通行证

EV证书在以下场景中具有不可替代性:

  • Windows内核驱动开发:微软要求所有64位内核模式驱动必须通过EV签名,且需通过WHQL认证,否则无法加载运行。OV证书无法满足WHQL认证的核心需求。
  • 金融/医疗等高敏感行业:涉及用户隐私数据或资金安全的软件需满足等保2.0三级要求,EV证书的审核流程天然满足PCIDSS、HIPAA等行业法规的合规要求。
  • 物联网与工业控制:智能汽车车载系统、工业PLC设备对软件安全性要求极高,EV证书签名的固件更新包能被设备系统直接信任并安装。

三、总结

EV代码签名证书通过穿透式身份核验、硬件级私钥保护、系统级即时信任三大核心优势,为开发者提供了从代码到用户的全链路安全屏障。虽然其审核周期较长(3-7个工作日)、成本高于OV证书,但在高安全需求场景中,它所提供的信任价值和合规保障是普通证书无法替代的。对于追求专业度与用户信任的开发者而言,EV证书不仅是一项技术投资,更是软件品牌资产的长期积累。







方舟 Coding Plan 支持 Doubao 、GLM 、DeepSeek 、Kimi 、MiniMax 等模型,工具不限,现在订阅 9 折,低至 36 元,订阅越多越划算!立即订阅: https://volcengine.com/L/D5ATMsA5zYg/ 邀请码:8BC2P3CX

aff 购买九折,介意的 v 友使用下面的
无 aff 地址: https://www.volcengine.com/activity/codingplan

代码签名的技术与合规角度,“未知开发者 / 未知发布者” 的核心原因是:你的软件没有被系统信任的数字证书签名,导致操作系统(Windows SmartScreen /macOS Gatekeeper)无法验证发布者身份与代码完整性。

下面从原理 → 证书选型 → Windows 签名 → macOS 签名 → 长期信任完整说明。

    • *

一、为什么未签名会报 “未知开发者”?

系统安全机制的核心判断:

  • 身份不可信:没有权威 CA(证书颁发机构)签发的代码签名证书,系统无法确认 “软件是谁做的”
  • 完整性风险:未签名文件可能被篡改、植入病毒,签名一旦被修改就会失效
  • 信誉机制:Windows SmartScreen 会对新证书 / 未签名软件强制警告,EV 证书可直接绕过

只有正规代码签名才能彻底消除警告,用户端看到 “发布者:XX 公司”。

    • *

二、代码签名证书类型与选型(Windows)

1. OV 代码签名(组织验证,主流)

  • 效果:显示公司名,新证书前 2–8 周仍有警告,积累信誉后自动消失
  • 适用:常规软件、工具、安装包(.exe/.msi/.dll)
  • 审核:1–3 天,需营业执照、法人信息

2. EV 代码签名(扩展验证,最高信任)

  • 效果立即消除 SmartScreen 警告,无过渡期
  • 价格:¥4,000–8,000 / 年
  • 强制场景内核驱动、Windows 10/11 驱动签名、WHQL 认证
  • 安全:私钥存USB Token 硬件(不可导出)
  • 审核:5–7 天,更严格的企业核验
    • *

代码签名证书https://www.joyssl.com/certificate/select/code_signing.html?n...

三、Windows 完整代码签名流程(彻底解决 “未知发布者”)

1. 申请证书(CA 机构)

主流 CA:DigiCert、Sectigo、GlobalSign、JoySSL(国内)

  • 准备材料:

    • 企业:营业执照、法人身份证、对公账户验证
    • 个人:个体户执照 + 身份证
  • 生成 CSR(证书签名请求)

    powershell

    # PowerShell生成
    New-SelfSignedCertificate -Type CodeSigningCert -Subject "CN=你的公司名" -KeyUsage DigitalSignature
  • 提交 CA 审核 → 获取 .pfx 证书(OV)或 USB Token(EV)

2. 用 SignTool 签名(Windows SDK)

(1)安装 SignTool

  • 安装 Windows SDK 或 Visual Studio(含签名工具)

(2)签名命令(必加时间戳!)

cmd

signtool sign /f 证书.pfx /p 证书密码 /fd SHA256 /tr http://timestamp.digicert.com /td SHA256 你的软件.exe
  • /f:证书文件
  • /p:证书密码
  • /fd SHA256:签名算法(必须 SHA256,旧 SHA1 无效)
  • /tr时间戳服务器(关键!避免证书过期后签名失效)

(3)验证签名

cmd

signtool verify /pa /v 你的软件.exe
  • 成功:显示 “数字签名详细信息”→ 发布者为你的公司名

3. 提升 SmartScreen 信誉(OV 证书必做)

  • 保持同一证书长期签名、持续分发
  • 提交微软 SmartScreen 信誉申请
  • 避免报毒:签名后用杀毒软件扫描、不捆绑恶意程序
    • *

四、macOS 代码签名(解决 “无法打开,因为来自未知开发者”)

1. 前提:苹果开发者账号

  • 年费:¥688 / 年(个人 / 公司)
  • 申请:Apple Developer → 实名认证

2. 证书类型

  • Developer ID Application:用于非 App Store 分发(直接官网下载)Apple Developer

3. 签名 + 公证(macOS 10.15+ 必须)

(1)用 codesign 签名

bash

运行

# 签名(--deep递归签内嵌组件)
codesign --force --deep --sign "Developer ID Application: 你的公司名 (TeamID)" --timestamp 你的App.app
  • --timestamp:添加时间戳(必加)
  • 查看证书:security find-identity -v -p codesigning

(2)苹果公证(Notarize)—— 强制!

bash

运行

# 上传公证
xcrun notarytool submit 你的App.zip --apple-id 你的邮箱 --password 专用密码 --team-id 你的TeamID

# 查看日志
xcrun notarytool log 提交ID --apple-id 你的邮箱 --password 专用密码 --team-id 你的TeamID

#  stapler 绑定公证结果(离线也可验证)
xcrun stapler staple 你的App.app
  • 公证通过后:彻底消除 Gatekeeper 警告

(3)验证

bash

运行

codesign -vvv 你的App.app
spctl -a -vvv 你的App.app
  • 成功:acceptedsource=Developer ID
    • *

五、关键最佳实践(必看)

  1. 必须加时间戳

    • 无时间戳:证书过期 → 签名失效 → 重新报未知发布者
  2. SHA256 算法

    • SHA1 已被系统弃用,必须用 SHA256
  3. 证书安全

    • EV 私钥存硬件 Token,不导出、不泄露
    • OV 证书(.pfx)加密保存,权限最小化
  4. 驱动程序特殊要求

    • Windows 10/11:必须 EV 签名 + WHQL 认证
    • 未签名驱动:无法安装(拦截率≈100%)
  5. 避免无效方案

    • 自签名、免费 SSL 证书(Let’s Encrypt)不能用于代码签名
    • *

六、效果对比(签名 vs 未签名)

表格

状态用户看到系统警告转化率
未签名未知发布者红色强拦截低(流失 70%+)
OV 签名(新)公司名轻度警告
OV(信誉满)公司名无警告
EV 签名公司名立即无警告最高
macOS 签名 + 公证可信开发者无警告
    • *

七、总结(最简路径)

  1. Windows

    • 常规软件:买 OV 代码签名证书 → SignTool SHA256 + 时间戳 → 积累信誉
    • 驱动:EV 证书 + WHQL
  2. macOS

    • 苹果开发者账号 → Developer ID 签名 + 公证

一句话正规代码签名是唯一彻底解决 “未知开发者” 的方法,临时绕过只是权宜之计。

问题:用其普通方法对比(图1)无法对比到其主模块内子模块(gitsubmodule)的diff(图2)
image.png

image.png

解决:
先用命令输出整体的diff
git diff --submodule=diff you-project-v1.7.7 you-project-v1.7.6 > full_diff.patch
再用apply patch(图1) ,
image.png
然后引用打的patch文件包,如下图,sub_* 相关的子模块就可以愉快的看diff了
image.png

前面一篇文章,我们手写了了一个mini版的Tomcat,接下来我们从源码和架构的角度来学习Tomcat

引入

Tomcat和Catalina是什么关系?

Tomcat的前身为Catalina,Catalina又是一个轻量级的Servlet容器。在美国,catalina是一个很美的小岛。所以Tomcat作者的寓意可能是想把Tomcat设计成一个优雅美丽且轻量级的web服务器。Tomcat从4.x版本开始除了作为支持Servlet的容器外,额外加入了很多的功能,比如:jsp、el、naming等等,所以说Tomcat不仅仅是Catalina

什么是Servlet?

所谓Servlet,其实就是Sun为了让Java能实现动态可交互的网页,从而进入Web编程领域而制定的一套标准!

在互联网兴起之初,当时的Sun公司(后面被Oracle收购)已然看到了这次机遇,于是设计出了Applet来对Web应用的支持。不过事实却并不是预期那么得好,Sun悲催地发现Applet并没有给业界带来多大的影响。经过反思,Sun就想既然机遇出现了,市场前景也非常不错,总不能白白放弃了呀,怎么办呢?于是又投入精力去搞一套规范出来,这时Servlet诞生了!

一个Servlet主要做下面三件事情:

  • 创建并填充Request对象,包括:URI、参数、method、请求头信息、请求体信息等
  • 创建Response对象
  • 执行业务逻辑,将结果通过Response的输出流输出到客户端

Servlet没有main方法,所以,如果要执行,则需要在一个容器里面才能执行,这个容器就是为了支持Servlet的功能而存在,Tomcat其实就是一个Servlet容器的实现

核心架构设计

官网:https://tomcat.apache.org/tomcat-8.0-doc/architecture/overvie...

Tomcat 的架构设计以 ‌模块化、分层、解耦‌ 为核心,遵循 Java Servlet 规范,同时支持高性能、高扩展的 Web 服务。其整体架构可概括为 ‌“连接器(Connector)- 容器(Container)” 双层模型‌,并通过 ‌Lifecycle 生命周期管理机制‌ 和 ‌责任链模式(Pipeline-Valve)‌ 实现组件协同。

Tomcat的架构呈“套娃式”嵌套:Server → Service → (Connector + Engine) → Host → Context → Wrapper

核心架构组成:

  • Server‌:代表整个 Tomcat 实例,是顶级容器,管理多个 Service。
  • Service‌:将一个或多个 Connector 与一个 Engine 绑定,构成独立服务单元。

    • Manager:管理器,用于管理会话Session
    • Logger:日志器,用于管理日志
    • Loader:加载器,和类加载有关,只会开放给Context所使用
    • Pipeline:管道组件,配合Valve实现过滤器功能
    • Valve:阀门组件,配合Pipeline实现过滤器功能
    • Realm:认证授权组件
  • Connector(连接器)‌:负责处理外部 HTTP/AJP 请求,实现网络通信与协议解析。
  • Container(容器)‌:负责加载和管理 Servlet,处理业务逻辑,包含四级嵌套容器:

    • Engine‌:处理所有请求,每个 Service 仅有一个。
    • Host‌:虚拟主机,对应一个域名或 IP。
    • Context‌:Web 应用上下文,对应一个 WAR 包或目录。
    • Wrapper‌:最底层容器,封装单个 Servlet。

从web.xml配置和模块对应角度

上述模块的理解不是孤立的,它可以直接映射为Tomcat的web.xml配置,让我们联系起来看

<Server port="8005" shutdown="SHUTDOWN">
  <Listener className="org.apache.catalina.startup.VersionLoggerListener" />

  <Listener className="org.apache.catalina.core.AprLifecycleListener" SSLEngine="on" />

  <Listener className="org.apache.catalina.core.JreMemoryLeakPreventionListener" />
  <Listener className="org.apache.catalina.mbeans.GlobalResourcesLifecycleListener" />
  <Listener className="org.apache.catalina.core.ThreadLocalLeakPreventionListener" />

  <GlobalNamingResources>
    <Resource name="UserDatabase" auth="Container"
              type="org.apache.catalina.UserDatabase"
              description="User database that can be updated and saved"
              factory="org.apache.catalina.users.MemoryUserDatabaseFactory"
              pathname="conf/tomcat-users.xml" />
  </GlobalNamingResources>

  <Service name="Catalina">

    <Connector port="8080" protocol="HTTP/1.1"
               connectionTimeout="20000"
               redirectPort="8443" />
    <Engine name="Catalina" defaultHost="localhost">
      <Realm className="org.apache.catalina.realm.LockOutRealm">
        <Realm className="org.apache.catalina.realm.UserDatabaseRealm"
               resourceName="UserDatabase"/>
      </Realm>

      <Host name="localhost"  appBase="webapps"
            unpackWARs="true" autoDeploy="true">
        <Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"
               prefix="localhost_access_log" suffix=".txt"
               pattern="%h %l %u %t &quot;%r&quot; %s %b" />

      </Host>
    </Engine>
  </Service>
</Server>

从一个完整请求的角度来看

通过一个完整的HTTP请求,我们还需要把它贯穿起来

假设来自客户的请求为:http://localhost:8080/test/index.jsp 请求被发送到本机端口8080,被在那里侦听的Coyote HTTP/1.1 Connector,然后

  • Connector把该请求交给它所在的Service的Engine来处理,并等待Engine的回应
  • Engine获得请求localhost:8080/test/index.jsp,匹配它所有虚拟主机Host
  • Engine匹配到名为localhost的Host(即使匹配不到也把请求交给该Host处理,因为该Host被定义为该Engine的默认主机)
  • localhost Host获得请求/test/index.jsp,匹配它所拥有的所有Context
  • Host匹配到路径为/test的Context(如果匹配不到就把该请求交给路径名为""的Context去处理)
  • path="/test"的Context获得请求/index.jsp,在它的mapping table中寻找对应的servlet
  • Context匹配到URL PATTERN为*.jsp的servlet,对应于JspServlet类,构造HttpServletRequest对象和HttpServletResponse对象,作为参数调用JspServlet的doGet或doPost方法
  • Context把执行完了之后的HttpServletResponse对象返回给Host
  • Host把HttpServletResponse对象返回给Engine
  • Engine把HttpServletResponse对象返回给Connector
  • Connector把HttpServletResponse对象返回给客户browser

从源码的设计角度看

从功能的角度将Tomcat源代码分成5个子模块,分别是:
  • Jsper模: 这个子模块负责jsp页面的解析、jsp属性的验证,同时也负责将jsp页面动态转换为java代码并编译成class文件。在Tomcat源代码中,凡是属于org.apache.jasper包及其子包中的源代码都属于这个子模块;
  • Servlet和Jsp模块: 这个子模块的源代码属于javax.servlet包及其子包,如我们非常熟悉的javax.servlet.Servlet接口、javax.servet.http.HttpServlet类及javax.servlet.jsp.HttpJspPage就位于这个子模块中;
  • Catalina模块: 这个子模块包含了所有以org.apache.catalina开头的java源代码。该子模块的任务是规范了Tomcat的总体架构,定义了Server、Service、Host、Connector、Context、Session及Cluster等关键组件及这些组件的实现,这个子模块大量运用了Composite设计模式。同时也规范了Catalina的启动及停止等事件的执行流程。从代码阅读的角度看,这个子模块应该是我们阅读和学习的重点。
  • Connector模块: 如果说上面三个子模块实现了Tomcat应用服务器的话,那么这个子模块就是Web服务器的实现。所谓连接器(Connector)就是一个连接客户和应用服务器的桥梁,它接收用户的请求,并把用户请求包装成标准的Http请求(包含协议名称,请求头Head,请求方法是Get还是Post等等)。同时,这个子模块还按照标准的Http协议,负责给客户端发送响应页面,比如在请求页面未发现时,connector就会给客户端浏览器发送标准的Http 404错误响应页面。
  • Resource模块: 这个子模块包含一些资源文件,如Server.xml及Web.xml配置文件。严格说来,这个子模块不包含java源代码,但是它还是Tomcat编译运行所必需的。

从后续深入理解的角度

我们看完上述组件结构后,后续应该重点从哪些角度深入理解Tomcat呢?
  • 基于组件的架构

我们知道组成Tomcat的是各种各样的组件,每个组件各司其职,组件与组件之间有明确的职责划分,同时组件与组件之间又通过一定的联系相互通信。Tomcat整体就是一个个组件的堆砌!

  • 基于JMX

我们在后续阅读Tomcat源码的时候,会发现代码里充斥着大量的类似于下面的代码。

Registry.getRegistry(null, null).invoke(mbeans, "init", false);
Registry.getRegistry(null, null).invoke(mbeans, "start", false);

而这实际上就是通过JMX来管理相应对象的代码。这儿我们不会详细讲述什么是JMX,我们只是简单地说明一下JMX的概念,参考JMX百度百科。

JMX(Java Management Extensions,即Java管理扩展)是一个为应用程序、设备、系统等植入管理功能的框架。JMX可以跨越一系列异构操作系统平台、系统体系结构和网络传输协议,灵活的开发无缝集成的系统、网络和服务管理应用。
  • 基于生命周期

如果我们查阅各个组件的源代码,会发现绝大多数组件实现了Lifecycle接口,这也就是我们所说的基于生命周期。生命周期的各个阶段的触发又是基于事件的方式。

启动过程详解

总体流程

我们看下整体的初始化和启动的流程,在理解的时候可以直接和Tomcat架构设计中组件关联上

启动过程代码浅析

看了下网上关于Tomcat的文章,很多直接关注在纯代码的分析,这种是很难的;我建议你一定要把代码加载进来自己看一下,然后这里我把它转化为核心的几个问题来帮助你理解。

Bootstrap主入口?

Tomcat源码就从它的main方法开始。Tomcat的main方法在org.apache.catalina.startup.Bootstrap 里。让我们带着这个为看下Catalina的初始化的

/**
  * 初始化守护进程
  * 
  * @throws Exception Fatal initialization error
  */
public void init() throws Exception {

    // 初始化classloader(包括catalinaLoader),下文将具体分析
    initClassLoaders();

    // 设置当前的线程的contextClassLoader为catalinaLoader
    Thread.currentThread().setContextClassLoader(catalinaLoader);

    SecurityClassLoad.securityClassLoad(catalinaLoader);

    // 通过catalinaLoader加载Catalina,并初始化startupInstance 对象
    if (log.isDebugEnabled())
        log.debug("Loading startup class");
    Class<?> startupClass = catalinaLoader.loadClass("org.apache.catalina.startup.Catalina");
    Object startupInstance = startupClass.getConstructor().newInstance();

    // 通过反射调用了setParentClassLoader 方法
    if (log.isDebugEnabled())
        log.debug("Setting startup class properties");
    String methodName = "setParentClassLoader";
    Class<?> paramTypes[] = new Class[1];
    paramTypes[0] = Class.forName("java.lang.ClassLoader");
    Object paramValues[] = new Object[1];
    paramValues[0] = sharedLoader;
    Method method =
        startupInstance.getClass().getMethod(methodName, paramTypes);
    method.invoke(startupInstance, paramValues);

    catalinaDaemon = startupInstance;

}

通过上面几行关键代码的注释,我们就可以看出Catalina是如何初始化的。这里还留下一个问题,tomcat为什么要初始化不同的classloader呢?我们将在下文进行详解。

Bootstrap如何初始化Catalina的?

我们用Sequence Diagram插件来看main方法的时序图,但是可以发现它并没有帮我们画出Bootstrap初始化Catalina的过程,这和上面的组件初始化不符合?

让我们带着这个为看下Catalina的初始化的

/**
  * 初始化守护进程
  * 
  * @throws Exception Fatal initialization error
  */
public void init() throws Exception {

    // 初始化classloader(包括catalinaLoader),下文将具体分析
    initClassLoaders();

    // 设置当前的线程的contextClassLoader为catalinaLoader
    Thread.currentThread().setContextClassLoader(catalinaLoader);

    SecurityClassLoad.securityClassLoad(catalinaLoader);

    // 通过catalinaLoader加载Catalina,并初始化startupInstance 对象
    if (log.isDebugEnabled())
        log.debug("Loading startup class");
    Class<?> startupClass = catalinaLoader.loadClass("org.apache.catalina.startup.Catalina");
    Object startupInstance = startupClass.getConstructor().newInstance();

    // 通过反射调用了setParentClassLoader 方法
    if (log.isDebugEnabled())
        log.debug("Setting startup class properties");
    String methodName = "setParentClassLoader";
    Class<?> paramTypes[] = new Class[1];
    paramTypes[0] = Class.forName("java.lang.ClassLoader");
    Object paramValues[] = new Object[1];
    paramValues[0] = sharedLoader;
    Method method =
        startupInstance.getClass().getMethod(methodName, paramTypes);
    method.invoke(startupInstance, paramValues);

    catalinaDaemon = startupInstance;

}

通过上面几行关键代码的注释,我们就可以看出Catalina是如何初始化的。这里还留下一个问题,tomcat为什么要初始化不同的classloader呢?我们将在下文进行详解。

启动过程:类加载机制详解

Tomcat初始化了哪些classloader

在Bootstrap中我们可以看到有如下三个classloader

ClassLoader commonLoader = null;
ClassLoader catalinaLoader = null;
ClassLoader sharedLoader = null;
如何初始化的呢?
private void initClassLoaders() {
    try {
        // commonLoader初始化
        commonLoader = createClassLoader("common", null);
        if (commonLoader == null) {
            // no config file, default to this loader - we might be in a 'single' env.
            commonLoader = this.getClass().getClassLoader();
        }
        // catalinaLoader初始化, 父classloader是commonLoader
        catalinaLoader = createClassLoader("server", commonLoader);
        // sharedLoader初始化
        sharedLoader = createClassLoader("shared", commonLoader);
    } catch (Throwable t) {
        handleThrowable(t);
        log.error("Class loader creation threw exception", t);
        System.exit(1);
    }
}
可以看出,catalinaLoader 和 sharedLoader 的 parentClassLoader 是 commonLoader。
如何创建classLoader的?

不妨再看下如何创建的?

private ClassLoader createClassLoader(String name, ClassLoader parent)
    throws Exception {

    String value = CatalinaProperties.getProperty(name + ".loader");
    if ((value == null) || (value.equals("")))
        return parent;

    value = replace(value);

    List<Repository> repositories = new ArrayList<>();

    String[] repositoryPaths = getPaths(value);

    for (String repository : repositoryPaths) {
        // Check for a JAR URL repository
        try {
            @SuppressWarnings("unused")
            URL url = new URL(repository);
            repositories.add(new Repository(repository, RepositoryType.URL));
            continue;
        } catch (MalformedURLException e) {
            // Ignore
        }

        // Local repository
        if (repository.endsWith("*.jar")) {
            repository = repository.substring
                (0, repository.length() - "*.jar".length());
            repositories.add(new Repository(repository, RepositoryType.GLOB));
        } else if (repository.endsWith(".jar")) {
            repositories.add(new Repository(repository, RepositoryType.JAR));
        } else {
            repositories.add(new Repository(repository, RepositoryType.DIR));
        }
    }

    return ClassLoaderFactory.createClassLoader(repositories, parent);
}

方法的逻辑也比较简单就是从 catalina.property文件里找 common.loader, shared.loader, server.loader 对应的值,然后构造成Repository 列表,再将Repository 列表传入ClassLoaderFactory.createClassLoader 方法,ClassLoaderFactory.createClassLoader 返回的是 URLClassLoader,而Repository 列表就是这个URLClassLoader 可以加在的类的路径。 在catalina.property文件里

common.loader="${catalina.base}/lib","${catalina.base}/lib/*.jar","${catalina.home}/lib","${catalina.home}/lib/*.jar"
server.loader=
shared.loader=

其中 shared.loader, server.loader 是没有值的,createClassLoader 方法里如果没有值的话,就返回传入的 parent ClassLoader,也就是说,commonLoader,catalinaLoader,sharedLoader 其实是一个对象。在Tomcat之前的版本里,这三个是不同的URLClassLoader对象。

Class<?> startupClass = catalinaLoader.loadClass("org.apache.catalina.startup.Catalina");
        Object startupInstance = startupClass.getConstructor().newInstance();

初始化完三个ClassLoader对象后,init() 方法就使用 catalinaClassLoader 加载了org.apache.catalina.startup.Catalina 类,并创建了一个对象,然后通过反射调用这个对象的 setParentClassLoader 方法,传入的参数是 sharedClassLoader。最后吧这个 Catania 对象复制给 catalinaDaemon 属性。

深入理解

可以复习下类加载机制的基础:解密类加载机制:深入理解JVM如何加载你的代码

什么是类加载机制

Java是一门面向对象的语言,而对象又必然依托于类。类要运行,必须首先被加载到内存。我们可以简单地把类分为几类:

  • Java自带的核心类
  • Java支持的可扩展类
  • 我们自己编写的类
  • 为什么要设计多个类加载器
如果所有的类都使用一个类加载器来加载,会出现什么问题呢?

假如我们自己编写一个类java.util.Object,它的实现可能有一定的危险性或者隐藏的bug。而我们知道Java自带的核心类里面也有java.util.Object,如果JVM启动的时候先行加载的是我们自己编写的java.util.Object,那么就有可能出现安全问题!

所以,Sun(后被Oracle收购)采用了另外一种方式来保证最基本的、也是最核心的功能不会被破坏。你猜的没错,那就是双亲委派模式!

  • 什么是双亲委派模型
双亲委派模型解决了类错乱加载的问题,也设计得非常精妙。

双亲委派模式对类加载器定义了层级,每个类加载器都有一个父类加载器。在一个类需要加载的时候,首先委派给父类加载器来加载,而父类加载器又委派给祖父类加载器来加载,以此类推。如果父类及上面的类加载器都加载不了,那么由当前类加载器来加载,并将被加载的类缓存起来。

所以上述类是这么加载的

  • Java自带的核心类 -- 由启动类加载器加载
  • Java支持的可扩展类 -- 由扩展类加载器加载
  • 我们自己编写的类 -- 默认由应用程序类加载器或其子类加载
但它也不是万能的,在有些场景也会遇到它解决不了的问题,比如如下场景。
双亲委派模型问题是如何解决的?
在Java核心类里面有SPI(Service Provider Interface),它由Sun编写规范,第三方来负责实现。SPI需要用到第三方实现类。如果使用双亲委派模型,那么第三方实现类也需要放在Java核心类里面才可以,不然的话第三方实现类将不能被加载使用。但是这显然是不合理的!怎么办呢?

ContextClassLoader(上下文类加载器)就来解围了。

在java.lang.Thread里面有两个方法,get/set上下文类加载器

public void setContextClassLoader(ClassLoader cl)
public ClassLoader getContextClassLoader()

我们可以通过在SPI类里面调用getContextClassLoader来获取第三方实现类的类加载器。由第三方实现类通过调用setContextClassLoader来传入自己实现的类加载器, 这样就变相地解决了双亲委派模式遇到的问题。

为什么Tomcat的类加载器也不是双亲委派模型
我们知道,Java默认的类加载机制是通过双亲委派模型来实现的,而Tomcat实现的方式又和双亲委派模型有所区别。

原因在于一个Tomcat容器允许同时运行多个Web程序,每个Web程序依赖的类又必须是相互隔离的。因此,如果Tomcat使用双亲委派模式来加载类的话,将导致Web程序依赖的类变为共享的。

举个例子,假如我们有两个Web程序,一个依赖A库的1.0版本,另一个依赖A库的2.0版本,他们都使用了类xxx.xx.Clazz,其实现的逻辑因类库版本的不同而结构完全不同。那么这两个Web程序的其中一个必然因为加载的Clazz不是所使用的Clazz而出现问题!而这对于开发来说是非常致命的!

Tomcat类加载机制是怎么样的呢
既然Tomcat的类加载机器不同于双亲委派模式,那么它又是一种怎样的模式呢?

我们在这里一定要看下官网提供的类加载的文档

结合经典的类加载机制,我们完整的看下Tomcat类加载图

我们在这张图中看到很多类加载器,除了Jdk自带的类加载器,我们尤其关心Tomcat自身持有的类加载器。仔细一点我们很容易发现:Catalina类加载器和Shared类加载器,他们并不是父子关系,而是兄弟关系。为啥这样设计,我们得分析一下每个类加载器的用途,才能知晓。

  • Common类加载器,负责加载Tomcat和Web应用都复用的类

    • Catalina类加载器,负责加载Tomcat专用的类,而这些被加载的类在Web应用中将不可见
    • Shared类加载器,负责加载Tomcat下所有的Web应用程序都复用的类,而这些被加载的类在Tomcat中将不可见

      • WebApp类加载器,负责加载具体的某个Web应用程序所使用到的类,而这些被加载的类在Tomcat和其他的Web应用程序都将不可见
      • Jsp类加载器,每个jsp页面一个类加载器,不同的jsp页面有不同的类加载器,方便实现jsp页面的热插拔

同样的,我们可以看到通过ContextClassLoader(上下文类加载器)的setContextClassLoader来传入自己实现的类加载器

public void init() throws Exception {

  initClassLoaders();

  // 看这里
  Thread.currentThread().setContextClassLoader(catalinaLoader);

  SecurityClassLoad.securityClassLoad(catalinaLoader);
...
WebApp类加载器
到这儿,我们隐隐感觉到少分析了点什么!没错,就是WebApp类加载器。整个启动过程分析下来,我们仍然没有看到这个类加载器。它又是在哪儿出现的呢?

我们知道WebApp类加载器是Web应用私有的,而每个Web应用其实算是一个Context,那么我们通过Context的实现类应该可以发现。在Tomcat中,Context的默认实现为StandardContext,我们看看这个类的startInternal()方法,在这儿我们发现了我们感兴趣的WebApp类加载器。

protected synchronized void startInternal() throws LifecycleException {
    if (getLoader() == null) {
        WebappLoader webappLoader = new WebappLoader(getParentClassLoader());
        webappLoader.setDelegate(getDelegate());
        setLoader(webappLoader);
    }
}

入口代码非常简单,就是webappLoader不存在的时候创建一个,并调用setLoader方法。我们接着分析setLoader

public void setLoader(Loader loader) {

    Lock writeLock = loaderLock.writeLock();
    writeLock.lock();
    Loader oldLoader = null;
    try {
        // Change components if necessary
        oldLoader = this.loader;
        if (oldLoader == loader)
            return;
        this.loader = loader;

        // Stop the old component if necessary
        if (getState().isAvailable() && (oldLoader != null) &&
            (oldLoader instanceof Lifecycle)) {
            try {
                ((Lifecycle) oldLoader).stop();
            } catch (LifecycleException e) {
                log.error("StandardContext.setLoader: stop: ", e);
            }
        }

        // Start the new component if necessary
        if (loader != null)
            loader.setContext(this);
        if (getState().isAvailable() && (loader != null) &&
            (loader instanceof Lifecycle)) {
            try {
                ((Lifecycle) loader).start();
            } catch (LifecycleException e) {
                log.error("StandardContext.setLoader: start: ", e);
            }
        }
    } finally {
        writeLock.unlock();
    }

    // Report this property change to interested listeners
    support.firePropertyChange("loader", oldLoader, loader);
}

这儿,我们感兴趣的就两行代码:

((Lifecycle) oldLoader).stop(); // 旧的加载器停止
((Lifecycle) loader).start(); // 新的加载器启动

启动过程:Catalina的加载

Catalina的引入

通过前面,我们知道了Tomcat的类加载机制和整体的组件加载流程;我们也知道通过Bootstrap初始化的catalinaClassLoader加载了Catalina,那么进而引入了一个问题就是Catalina是如何加载的呢?加载了什么呢?
  • 先回顾下整个流程,和我们分析的阶段

  • 看下Bootstrap中Load的过程
/**
  * 加载守护进程
  */
private void load(String[] arguments) throws Exception {

    // Call the load() method
    String methodName = "load";
    Object param[];
    Class<?> paramTypes[];
    if (arguments==null || arguments.length==0) {
        paramTypes = null;
        param = null;
    } else {
        paramTypes = new Class[1];
        paramTypes[0] = arguments.getClass();
        param = new Object[1];
        param[0] = arguments;
    }
    Method method =
        catalinaDaemon.getClass().getMethod(methodName, paramTypes); 
    if (log.isDebugEnabled()) {
        log.debug("Calling startup class " + method);
    }
    method.invoke(catalinaDaemon, param);// 本质上就是调用catalina的load方法
}

Catalina的加载

上一步,我们知道catalina load的触发,因为有参数所以是load(String[])方法。我们进而看下这个load方法做了什么?

  • load(String[])本质上还是调用了load方法
/*
  * Load using arguments
  */
public void load(String args[]) {

    try {
        if (arguments(args)) { // 处理命令行的参数
            load();
        }
    } catch (Exception e) {
        e.printStackTrace(System.out);
    }
}
  • load加载过程本质上是初始化Server的实例
/**
  * Start a new server instance.
  */
public void load() {

    // 如果已经加载则退出
    if (loaded) {
        return;
    }
    loaded = true;

    long t1 = System.nanoTime();

    // (已经弃用)
    initDirs();

    // Before digester - it may be needed
    initNaming();

    // 解析 server.xml
    parseServerXml(true);
    Server s = getServer();
    if (s == null) {
        return;
    }

    getServer().setCatalina(this);
    getServer().setCatalinaHome(Bootstrap.getCatalinaHomeFile());
    getServer().setCatalinaBase(Bootstrap.getCatalinaBaseFile());

    // Stream redirection
    initStreams();

    // 启动Server
    try {
        getServer().init();
    } catch (LifecycleException e) {
        if (Boolean.getBoolean("org.apache.catalina.startup.EXIT_ON_INIT_FAILURE")) {
            throw new java.lang.Error(e);
        } else {
            log.error(sm.getString("catalina.initError"), e);
        }
    }

    if(log.isInfoEnabled()) {
        log.info(sm.getString("catalina.init", Long.toString(TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - t1))));
    }
}

总体流程如下:

initDirs

已经弃用了,Tomcat10会删除这个方法。

/**
  * @deprecated unused. Will be removed in Tomcat 10 onwards.
  */
@Deprecated
protected void initDirs() {
}
initNaming

设置额外的系统变量

protected void initNaming() {
  // Setting additional variables
  if (!useNaming) {
      log.info(sm.getString("catalina.noNaming"));
      System.setProperty("catalina.useNaming", "false");
  } else {
      System.setProperty("catalina.useNaming", "true");
      String value = "org.apache.naming";
      String oldValue =
          System.getProperty(javax.naming.Context.URL_PKG_PREFIXES);
      if (oldValue != null) {
          value = value + ":" + oldValue;
      }
      System.setProperty(javax.naming.Context.URL_PKG_PREFIXES, value);
      if( log.isDebugEnabled() ) {
          log.debug("Setting naming prefix=" + value);
      }
      value = System.getProperty
          (javax.naming.Context.INITIAL_CONTEXT_FACTORY);
      if (value == null) {
          System.setProperty
              (javax.naming.Context.INITIAL_CONTEXT_FACTORY,
                "org.apache.naming.java.javaURLContextFactory");
      } else {
          log.debug("INITIAL_CONTEXT_FACTORY already set " + value );
      }
  }
}
Server.xml的解析

分三大块,下面的代码还是很清晰的:

protected void parseServerXml(boolean start) {
    // Set configuration source
    ConfigFileLoader.setSource(new CatalinaBaseConfigurationSource(Bootstrap.getCatalinaBaseFile(), getConfigFile()));
    File file = configFile();

    if (useGeneratedCode && !Digester.isGeneratedCodeLoaderSet()) {
        // Load loader
        String loaderClassName = generatedCodePackage + ".DigesterGeneratedCodeLoader";
        try {
            Digester.GeneratedCodeLoader loader =
                    (Digester.GeneratedCodeLoader) Catalina.class.getClassLoader().loadClass(loaderClassName).newInstance();
            Digester.setGeneratedCodeLoader(loader);
        } catch (Exception e) {
            if (log.isDebugEnabled()) {
                log.info(sm.getString("catalina.noLoader", loaderClassName), e);
            } else {
                log.info(sm.getString("catalina.noLoader", loaderClassName));
            }
            // No loader so don't use generated code
            useGeneratedCode = false;
        }
    }

    // 初始化server.xml的位置
    File serverXmlLocation = null;
    String xmlClassName = null;
    if (generateCode || useGeneratedCode) {
        xmlClassName = start ? generatedCodePackage + ".ServerXml" : generatedCodePackage + ".ServerXmlStop";
    }
    if (generateCode) {
        if (generatedCodeLocationParameter != null) {
            generatedCodeLocation = new File(generatedCodeLocationParameter);
            if (!generatedCodeLocation.isAbsolute()) {
                generatedCodeLocation = new File(Bootstrap.getCatalinaHomeFile(), generatedCodeLocationParameter);
            }
        } else {
            generatedCodeLocation = new File(Bootstrap.getCatalinaHomeFile(), "work");
        }
        serverXmlLocation = new File(generatedCodeLocation, generatedCodePackage);
        if (!serverXmlLocation.isDirectory() && !serverXmlLocation.mkdirs()) {
            log.warn(sm.getString("catalina.generatedCodeLocationError", generatedCodeLocation.getAbsolutePath()));
            // Disable code generation
            generateCode = false;
        }
    }

    // 用 SAXParser 来解析 xml,解析完了之后,xml 里定义的各种标签就有对应的实现类对象了
    ServerXml serverXml = null;
    if (useGeneratedCode) {
        serverXml = (ServerXml) Digester.loadGeneratedClass(xmlClassName);
    }

    if (serverXml != null) {
        serverXml.load(this);
    } else {
        try (ConfigurationSource.Resource resource = ConfigFileLoader.getSource().getServerXml()) {
            // Create and execute our Digester
            Digester digester = start ? createStartDigester() : createStopDigester();
            InputStream inputStream = resource.getInputStream();
            InputSource inputSource = new InputSource(resource.getURI().toURL().toString());
            inputSource.setByteStream(inputStream);
            digester.push(this);
            if (generateCode) {
                digester.startGeneratingCode();
                generateClassHeader(digester, start);
            }
            digester.parse(inputSource);
            if (generateCode) {
                generateClassFooter(digester);
                try (FileWriter writer = new FileWriter(new File(serverXmlLocation,
                        start ? "ServerXml.java" : "ServerXmlStop.java"))) {
                    writer.write(digester.getGeneratedCode().toString());
                }
                digester.endGeneratingCode();
                Digester.addGeneratedClass(xmlClassName);
            }
        } catch (Exception e) {
            log.warn(sm.getString("catalina.configFail", file.getAbsolutePath()), e);
            if (file.exists() && !file.canRead()) {
                log.warn(sm.getString("catalina.incorrectPermissions"));
            }
        }
    }
}
initStreams

替换掉System.out, System.err为自定义的PrintStream

protected void initStreams() {
    // Replace System.out and System.err with a custom PrintStream
    System.setOut(new SystemLogHandler(System.out));
    System.setErr(new SystemLogHandler(System.err));
}

Catalina 的启动

在 load 方法之后,Tomcat 就初始化了一系列的组件,接着就可以调用 start 方法进行启动了。

/**
  * Start a new server instance.
  */
public void start() {

    if (getServer() == null) {
        load();
    }

    if (getServer() == null) {
        log.fatal(sm.getString("catalina.noServer"));
        return;
    }

    long t1 = System.nanoTime();

    // Start the new server
    try {
        getServer().start();
    } catch (LifecycleException e) {
        log.fatal(sm.getString("catalina.serverStartFail"), e);
        try {
            getServer().destroy();
        } catch (LifecycleException e1) {
            log.debug("destroy() failed for failed Server ", e1);
        }
        return;
    }

    long t2 = System.nanoTime();
    if(log.isInfoEnabled()) {
        log.info(sm.getString("catalina.startup", Long.valueOf((t2 - t1) / 1000000)));
    }

    // Register shutdown hook
    if (useShutdownHook) {
        if (shutdownHook == null) {
            shutdownHook = new CatalinaShutdownHook();
        }
        Runtime.getRuntime().addShutdownHook(shutdownHook);

        // If JULI is being used, disable JULI's shutdown hook since
        // shutdown hooks run in parallel and log messages may be lost
        // if JULI's hook completes before the CatalinaShutdownHook()
        LogManager logManager = LogManager.getLogManager();
        if (logManager instanceof ClassLoaderLogManager) {
            ((ClassLoaderLogManager) logManager).setUseShutdownHook(
                    false);
        }
    }

    if (await) {
        await();
        stop();
    }
}

上面这段代码,逻辑非常简单,首先确定 getServer() 方法不为 null ,也就是确定 server 属性不为null,而 server 属性是在 load 方法就初始化了。

整段代码的核心就是 try-catch 里的 getServer().start() 方法了,也就是调用 Server 对象的 start() 方法来启动 Tomcat。本篇文章就先不对 Server 的 start() 方法进行解析了,下篇文章会单独讲。

Catalina 的关闭

调用完 Server#start 方法之后,注册了一个ShutDownHook,也就是 CatalinaShutdownHook 对象,

/**
  * Shutdown hook which will perform a clean shutdown of Catalina if needed.
  */
protected class CatalinaShutdownHook extends Thread {

  @Override
  public void run() {
      try {
          if (getServer() != null) {
              Catalina.this.stop();
          }
      } catch (Throwable ex) {
          ExceptionUtils.handleThrowable(ex);
          log.error(sm.getString("catalina.shutdownHookFail"), ex);
      } finally {
          // If JULI is used, shut JULI down *after* the server shuts down
          // so log messages aren't lost
          LogManager logManager = LogManager.getLogManager();
          if (logManager instanceof ClassLoaderLogManager) {
              ((ClassLoaderLogManager) logManager).shutdown();
          }
      }
  }
}

CatalinaShutdownHook 的逻辑也简单,就是调用 Catalina 对象的 stop 方法来停止 tomcat。

最后就进入 if 语句了,await 是在 Bootstrap 里调用的时候设置为 true 的,也就是本文开头的时候提到的三个方法中的一个。await 方法的作用是停住主线程,等待用户输入shutdown 命令之后,停止等待,之后 main 线程就调用 stop 方法来停止Tomcat。

/**
  * Stop an existing server instance.
  */
public void stop() {

    try {
        // Remove the ShutdownHook first so that server.stop()
        // doesn't get invoked twice
        if (useShutdownHook) {
            Runtime.getRuntime().removeShutdownHook(shutdownHook);

            // If JULI is being used, re-enable JULI's shutdown to ensure
            // log messages are not lost
            LogManager logManager = LogManager.getLogManager();
            if (logManager instanceof ClassLoaderLogManager) {
                ((ClassLoaderLogManager) logManager).setUseShutdownHook(
                        true);
            }
        }
    } catch (Throwable t) {
        ExceptionUtils.handleThrowable(t);
        // This will fail on JDK 1.2. Ignoring, as Tomcat can run
        // fine without the shutdown hook.
    }

    // Shut down the server
    try {
        Server s = getServer();
        LifecycleState state = s.getState();
        if (LifecycleState.STOPPING_PREP.compareTo(state) <= 0
                && LifecycleState.DESTROYED.compareTo(state) >= 0) {
            // Nothing to do. stop() was already called
        } else {
            s.stop();
            s.destroy();
        }
    } catch (LifecycleException e) {
        log.error(sm.getString("catalina.stopError"), e);
    }

}

Catalina 的 stop 方法主要逻辑是调用 Server 对象的 stop 方法。

聊聊关闭钩子

上面我们看到CatalinaShutdownHook, 这里有必要谈谈JVM的关闭钩子。

if (shutdownHook == null) {
    shutdownHook = new CatalinaShutdownHook();
}
Runtime.getRuntime().addShutdownHook(shutdownHook);

关闭钩子是指通过Runtime.addShutdownHook注册的但尚未开始的线程。这些钩子可以用于实现服务或者应用程序的清理工作,例如删除临时文件,或者清除无法由操作系统自动清除的资源。

JVM既可以正常关闭,也可以强行关闭。正常关闭的触发方式有多种,包括:当最后一个“正常(非守护)”线程结束时,或者当调用了System.exit时,或者通过其他特定于平台的方法关闭时(例如发送了SIGINT信号或者键入Ctrl-C)。

正常关闭中,JVM首先调用所有已注册的关闭钩子。JVM并不能保证关闭钩子的调用顺序。在关闭应用程序线程时,如果有(守护或者非守护)线程仍然在执行,那么这些线程接下来将与关闭进程并发执行。当所有的关闭钩子都执行结束时,如果runFinalizersOnExit为true【通过Runtime.runFinalizersOnExit(true)设置】,那么JVM将运行这些Finalizer(对象重写的finalize方法),然后再停止。JVM不会停止或中断任何在关闭时仍然运行的应用程序线程。当JVM最终结束时,这些线程将被强行结束。如果关闭钩子或者Finalizer没有执行完成,那么正常关闭进程“挂起”并且JVM必须被强行关闭。当JVM被强行关闭时,只是关闭JVM,并不会运行关闭钩子(举个例子,类似于电源都直接拔了,还怎么做其它动作呢?)。

下面是一个简单的示例:

public class T {
    @SuppressWarnings("deprecation")
    public static void main(String[] args) throws Exception {
        //启用退出JVM时执行Finalizer
        Runtime.runFinalizersOnExit(true);
        MyHook hook1 = new MyHook("Hook1");
        MyHook hook2 = new MyHook("Hook2");
        MyHook hook3 = new MyHook("Hook3");
        
        //注册关闭钩子
        Runtime.getRuntime().addShutdownHook(hook1);
        Runtime.getRuntime().addShutdownHook(hook2);
        Runtime.getRuntime().addShutdownHook(hook3);
        
        //移除关闭钩子
        Runtime.getRuntime().removeShutdownHook(hook3);
        
        //Main线程将在执行这句之后退出
        System.out.println("Main Thread Ends.");
    }
}

class MyHook extends Thread {
    private String name;
    public MyHook (String name) {
        this.name = name;
        setName(name);
    }
    public void run() {
        System.out.println(name + " Ends.");
    }
    //重写Finalizer,将在关闭钩子后调用
    protected void finalize() throws Throwable {
        System.out.println(name + " Finalize.");
    }
}

和(可能的)执行结果(因为JVM不保证关闭钩子的调用顺序,因此结果中的第二、三行可能出现相反的顺序):

Main Thread Ends.
Hook2 Ends.
Hook1 Ends.
Hook3 Finalize.
Hook2 Finalize.
Hook1 Finalize.

可以看到,main函数执行完成,首先输出的是Main Thread Ends,接下来执行关闭钩子,输出Hook2 Ends和Hook1 Ends。这两行也可以证实:JVM确实不是以注册的顺序来调用关闭钩子的。而由于hook3在调用了addShutdownHook后,接着对其调用了removeShutdownHook将其移除,于是hook3在JVM退出时没有执行,因此没有输出Hook3 Ends。

另外,由于MyHook类实现了finalize方法,而main函数中第一行又通过Runtime.runFinalizersOnExit(true)打开了退出JVM时执行Finalizer的开关,于是3个hook对象的finalize方法被调用,输出了3行Finalize。

注意,多次调用addShutdownHook来注册同一个关闭钩子将会抛出IllegalArgumentException:

Exception in thread "main" java.lang.IllegalArgumentException: Hook previously registered
    at java.lang.ApplicationShutdownHooks.add(ApplicationShutdownHooks.java:72)
    at java.lang.Runtime.addShutdownHook(Runtime.java:211)
    at T.main(T.java:12)

另外,从JavaDoc中得知:一旦JVM关闭流程开始,就只能通过调用halt方法来停止该流程,也不可能再注册或移除关闭钩子了,这些操作将导致抛出IllegalStateException

如果在关闭钩子中关闭应用程序的公共的组件,如日志服务,或者数据库连接等,像下面这样:

Runtime.getRuntime().addShutdownHook(new Thread() {
    public void run() {
        try { 
            LogService.this.stop();
        } catch (InterruptedException ignored){
            //ignored
        }
    }
});

由于关闭钩子将并发执行,因此在关闭日志时可能导致其他需要日志服务的关闭钩子产生问题为了避免这种情况,可以使关闭钩子不依赖那些可能被应用程序或其他关闭钩子关闭的服务。实现这种功能的一种方式是对所有服务使用同一个关闭钩子(而不是每个服务使用一个不同的关闭钩子),并且在该关闭钩子中执行一系列的关闭操作。这确保了关闭操作在单个线程中串行执行,从而避免了在关闭操作之前出现竞态条件或死锁等问题。

使用场景

通过Hook实现临时文件清理

public class test {

  public static void main(String[] args) {
      try {
          Thread.sleep(20000);
      } catch (InterruptedException e) {
          e.printStackTrace();
      }

      Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
          public void run() {
              System.out.println("auto clean temporary file");
          }
      }));
  }
}

小结

Catalina 类承接了 Bootstrap 类的 load 和 start 方法,然后根据配置初始化了 Tomcat 的组件,并调用了 Server 类的 init 和 start 方法来启动 Tomcat。

上周他们解决了 Extra usage 消失的问题,所以我又重新 subscribe 了。

4.7 发布以后的这一周,以前一直是周二 weekly reset 的,今天突然变成周四 reset 了,生生的少了两天的额度。都是卡着点用的,现在 100% 了告诉我两天后才能用。

这 Claude 两个活人客服都没有,refund 明说取消不退钱,这已经明抢了

大家好,我是R哥。

近日,Claude 又开始搞事情了,开始要求实名身份验证了。。

Claude 已经上线身份验证功能,针对一些特定场景你可能会看到身份验证提示。。。

Claude 公司简直特么丧心病狂啊。。。

验证条件有多苛刻?来看看吧:

1、需要一个身份证、护照、驾照、而且必须是原件,必须得拿在手上。。

2、一部手机或者带摄像头的电脑,可能会让你用手机或者电脑摄像头现场自拍一张。。

3、需要验证几分钟,官方说一般验证不到五分钟。

更离谱的是,验证通过,不代表你就稳了。。。

验证完成后可能还会封号:

比如,你的账号是在不支持的地区注册的、服务条款违规、未满 18 岁等等。。

中国是不支持的地区,而中国用户又是最多的,这针对性也太强了吧??

关键是,它不是单纯加强安全,而是在不断抬高普通用户的使用成本,尤其是对依赖 Claude Code 的人来说,这种不确定性会直接影响工作流。

以后上手 Claude Code 的成本会越来越高,也变得越来越不可控,这样恶心的公司,建议及时撤离选择其他 AI 编程工具,比如:Codex、Gemini CLI 等等。

如果一个工具动不动就让你担心封号、验证、限制地区,那它再强,也很难成为真正靠谱的生产力工具。

我个人目前主要就是 Codex、Gemini CLI 相辅相成,GPT-5.4、Gemini 3 Pro 都是顶尖编程模型,不管文本生成、还是编程能力,一点也不输 Claude Code,主打一个稳定、靠谱、好用

推荐教程:

好了,今天就暂时分享到这里了,R哥持续分享更多 AI 好玩的东西,R哥第一时间推送,关注我和我一起学 AI。

⚠️ 版权声明:

本文系公众号 "AI技术宅" 原创,未经授权禁止转载,严禁搬运、抄袭、洗稿、侵权一律投诉,并保留追究其法律责任的权利。

上架过 app 的都知道,上架时需要上传 app 截图预览宣传图,如果要好看的宣传图还不得不借用第三方软件,比如比较出名的 theapplaunchpad ,会员费用还高,现在 GPT Image2 直接杀死 screenshots 赛道

下面是我使用 GPT Image2 给我们播面 APP 出的预览图和原截图,图都是一次出的,图片角度倾斜也不会出现文字异常的问题,nano2 就是重绘会导致文字异常:

pe2GlM8.md.png

pe2GMxf.md.png

pe2G3qg.png

pe2Yz5j.md.png

原图:

pe2JN0e.md.png

pe2JtmD.md.png

pe2JG6K.md.png