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

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

2026 年 4 月,SpaceX 宣布与 AI 编程初创公司 Cursor 达成协议,获得在今年晚些时候以 600 亿美元收购该公司的选择权,或支付 100 亿美元达成深度合作。该交易旨在将 Cursor 的领先 AI 编程能力与 SpaceX 的 Colossus 超级计算机结合,以补齐其在 AI 编程工具领域的短板。

代码签名的技术与合规角度,“未知开发者 / 未知发布者” 的核心原因是:你的软件没有被系统信任的数字证书签名,导致操作系统(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

最近在折腾一个项目:WG-FRIEND

一句话介绍:

Semantic WireGuard/BoringTun lifecycle and client management helper

它的出发点其实很简单:
我这边最近比较常见的几个场景,是需要一台比较稳定的服务器做跨网络访问,需要远程回家,也需要把多台设备之间的 WireGuard 生命周期管理得更清楚一些。

但我一直觉得,现有这类方案里有个空档:

  • wg-quick 很好用,但更像“把接口拉起来”的工具
  • PiVPN 这类方案很适合快速起量,但整体还是偏 shell/script orchestration
  • BoringTun 很强,尤其是 Rust userspace WireGuard 这条路线很有价值,但它本身并不负责 manager / control plane

所以用 Rust 实现wg-friend 就此开始:将“拉起接口 / 管理服务 / 管理客户端 / 导入历史资产 / 做诊断”这些事情,从零散脚本提升成一个语义更明确的 control plane 。

目前这个项目主要做了几件事:

1. 把 WireGuard/BoringTun 的操作语义化

命令面我切成了四组:

  • server
  • client
  • service
  • doctor

我不太想继续沿用“全靠 shell 拼起来”的方式,而是想把常用动作收敛成更稳定的 CLI 语义。

2. 不再把客户端状态散落在各处

wg-friend 会把可完整物化的客户端,纳入 /etc/wg-friend 下面的 canonical state 。

也就是说,进入管理域的前提不是“这个客户端貌似存在过”,而是它必须足够完整,能产出:

  • 元数据
  • 标准导出配置
  • QR-ready payload

3. 给历史部署一条 import 路径

很多现有机器并不是从零开始的,已经有 /etc/wireguard、有过去导出的 client conf 、也可能混着 PiVPN 或手工维护的文件。

所以我做了 client import,去扫描本地已有客户端配置,校验完整性,推导公钥,对上 server peer set ,然后再写入 wg-friend 的 canonical state 。

我更希望这个项目能做的是:

让旧部署渐进迁移,而不是推倒重来。

4. 明确和 systemd / BoringTun 的职责边界

这里我比较明确的设计是:

  • systemd 去负责长期进程监督
  • wg-friend 去负责 preflight / configure / verify / cleanup
  • BoringTun 去负责 userspace WireGuard tunnel
  • wg-friend 不去碰协议实现,而是做 manager/control plane

协议实现、服务托管、运维语义,这三层最好不要混成一团。

5. Rust 实现,不做 TUI ,优先可维护性

这个项目是 Rust 写的。
我没有做 TUI ,而是更偏向:

  • 命令优先
  • 缺参时再 prompt
  • 输出尽量语义化
  • 诊断尽量可读
  • systemd 场景下行为尽量稳定

我想做的不是“一个很炫的界面”,而是一个真正能放到服务器上长期跑的 WireGuard/BoringTun helper 。


我现在对它的定位,大概就是:

BoringTun 不做 manager ,那这一层我来做。

如果你也有下面这些场景:

  • 需要一台稳定的服务器承载 WireGuard/BoringTun
  • 有远程回家需求
  • 有多客户端管理、导出、二维码分发需求
  • 机器上已经有历史 WireGuard 资产,不想推倒重来
  • 希望整个 lifecycle 比 wg-quick + shell 更清晰一些

欢迎看看,也欢迎直接拍砖。

目前还是比较早期,主要先把管理模型、状态模型和生命周期边界打清楚。

前面一篇文章,我们手写了了一个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。

一个中国AI,让硅谷最热独角兽公开道歉

它用美国顶尖实验室1%的资源,做出了让马斯克点赞、让Cursor跪地认错的模型。

你有没有想过——

当你打开 Cursor 写代码,当你在 Perplexity 搜索,当你用某个"美国产品"感叹"AI真厉害"的时候……

底层跑的,可能是一个北京团队写的模型。

这个团队叫月之暗面,他们的产品叫 Kimi

而就在2026年开年,他们做了一件让整个全球AI圈都没想到的事。


01 | 那个让Cursor道歉的故事

3月下旬,AI编程圈发生了一件"罗生门"级别的事件。

估值500亿美元的编程工具 Cursor,推出了新一代旗舰模型 Composer 2,主打"长周期智能体编程",宣传得有声有色。

然后——有人扒出来了。

一个网友操作了一下 base URL,发现里面藏着这样一串字符:

accounts/anysphere/models/kimi-k2p5-rl-0317-s515-fast

翻译一下:Cursor花了大价钱包装的旗舰模型,底座是 Kimi K2.5。

舆论炸了。马斯克转发评论。Cursor创始人不得不公开道歉。

月之暗面随后确认:双方通过 Fireworks AI 平台存在正式授权合作。

一个来自北京的开源模型,成了硅谷最热编程工具不敢说出口的秘密。

这一天,Kimi 赢得了任何广告都买不来的品牌溢价。


02 | K2.5到底强在哪里?

Kimi K2.5 于2026年1月27日正式发布开源。

发布20天内,Kimi的累计收入超过了2025年全年总收入。

1月订阅订单环比增长 8280%,在 Stripe 全球榜单上从百名开外直接冲进前十

但数字背后,真正的故事是技术。

🔹 它重新定义了"Agent"

过去的AI,是你问一句、它答一句。
K2.5 不一样——它是一个指挥官

它可以同时调度最多 100个子Agent分身,并行处理 1500个步骤,所有角色分配、任务拆解、最终验收,全部由主Agent自动完成。

这不是对话,这是自动化作战

🔹 它解决了长文本的速度死穴

处理100万字的文档,传统Transformer的计算量会呈平方级增长——越长越慢,慢到用不起。

Kimi 团队自研了 Kimi Linear,一种混合线性注意力架构,打破了"所有层必须全注意力"的行业惯例。

结果:在128K到100万字的超长上下文中,解码速度提升5到6倍

🔹 它用"注意力残差"重写了Transformer的记忆方式

过去模型的每一层,都无差别地叠加前面所有层的信息——重要的和不重要的一视同仁,层数越多,关键信息越被稀释。

Kimi 提出的 Attention Residuals(注意力残差),让模型像人一样"有选择地回忆"——每层根据当前需求,主动调取最值得参考的信息。

48B参数模型训练效率因此提升 1.25倍

这篇论文发布后,马斯克公开表示"令人印象深刻",OpenAI前研究副总裁 Jerry Tworek 评价:这标志着"深度学习2.0"时代的到来。

顺带一提,这篇论文的第一作者——是一个来自深圳的17岁在读高中生


03 | 一家公司,做了一个反常识的选择

2025年初,DeepSeek横空出世,以极低成本开源,让整个中国AI创业圈陷入存在危机。

外界的声音是:模型公司还有独立存在的价值吗?

月之暗面创始人杨植麟的回答,不是一句话,而是一个选择——

继续死磕基础模型,坚持开源,放弃短期流量,押注技术长期主义。

他们是全球第一个在超大规模训练中跑通 Muon优化器 的公司,解决了Adam优化器十年来的扩展瓶颈。

他们把 MuonClip、Kimi Linear、Attention Residuals 全部开源,贡献给全球开发者社区。

2026年3月,英伟达GTC大会,杨植麟成为唯一受邀现场演讲的中国独立大模型公司创始人,用40分钟向全球系统披露Kimi的技术路线图。

从北京出发,站上硅谷最大的舞台。


04 | 下一步,他们盯着什么?

4月,Kimi K2.6 Code Preview 已进入内测,专攻代码生成与Agent能力,预计5月正式发布。

与此同时,下一代旗舰 Kimi K3 已在研发中——据传参数规模将达到3到4万亿,直接对标美国头部模型。

月之暗面内部信透露的目标只有一个字:

超越 Anthropic。


写在最后

有人问,中国AI和美国AI的差距到底有多大?

K2.5 给出了一个答案:

当你以为在用美国产品的时候,你可能已经在用中国技术了。

这不是一个追赶的故事,这是一个已经发生的故事

而月之暗面,才刚开始。


如果你觉得这篇文章有价值,欢迎转发给关注AI的朋友。

关注我,持续追踪全球AI最新动态。

本文由mdnice多平台发布

一直按时滴药,眼压也降下来了,刚去医院检查回来,视野又有了一点进展,心里七上八下,医生忙得没工夫搭理你,问就是继续滴药不然就做激光或者手术。

有点无奈。。有没有患者朋友能交流下病情,或者青光眼患者交流群或者论坛啥的推荐?

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

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

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

哪位佬有土区注册教程?发现现在注册 Apple ID ,国家选择土耳其,手机号是国内,不开代理注册,在输入图片验证码环节就提示当前无法创建,后面代理挂了 hk 和新加坡全局,但是走到完邮箱验证吗和最后手机验证码环节后也提示当前无法创建此账号,请问还有什么办法现在可以创建土区账号或者尼区账号呢?

大家好,我是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技术宅" 原创,未经授权禁止转载,严禁搬运、抄袭、洗稿、侵权一律投诉,并保留追究其法律责任的权利。

写简历没思路,文笔写的像入..申请书,把我自己写笑了。主要自己实习不多。
看网上好多人说实习可以瞎编,但不是还有实习证明嘛,怕 hr 查最后弄得个信用不好,没有勇气编,也不知道怎么编。
把简历给 ai 改,可能是提示词的原因,改的也不行
如果有大佬能帮小弟提供思路方向,或者告诉我怎么对 ai 提问或者有哪些课,书能帮到我就行

📰 今日新闻精选:

  • 春假催热 “加长版五一”:旅游产品搜索热度环比增长 364%,县域精品民宿预订量同比涨 92%
  • 官方:到 2030 年服务业总规模迈上 100 万亿元台阶,培育更多 “中国服务” 品牌
  • 31 省份一季度人均可支配收入公布:17 省居民超万元,其中上海、北京、浙江三地超 2 万元
  • 第二届世界人形机器人运动会将于 8 月在京举办,新增拔河、举重等 8 个赛项
  • 全球电力评估报告:2025 年全球太阳能激增遏制化石燃料增长,清洁能源首次超越煤电
  • 宁德时代发布第三代神行超充电池:从 10% 充电到 98% 仅需 6 分 27 秒,全球最快速度
  • 2026 年一季度 AI 应用价值榜:近四成中国网民手机装 AI,豆包月活逼近 3.5 亿稳居第一,元宝跌出前三
  • 全国首个 Pre6G 试验网在江苏南京投入运行,具备高带宽、低时延等特点,相关能力可达 5G 的 10 倍
  • 微信宣布 5 国可用微信支付:韩国、斯里兰卡、泰国、马来西亚、新加坡
  • 天猫超市发布 “超喵 1.0”:全国首个线上 AI 超市智能体落地,为品牌配备专属 “经营专家”
  • 美媒:苹果官宣换帅,特努斯将接替库克担任 CEO,长期负责硬件工程,库克称他是最佳人选
  • 日媒:日本通过内阁决议正式允许出口杀伤性武器,首批交易或向菲律宾出口二手军舰
  • 俄媒:美国提出 “丝路七国 +” 倡议,拟打造中亚南亚新地缘格局
  • 美媒:特朗普称停火到期后将轰炸伊朗;美国多名退伍老兵在国会大厦抗议对伊战争,现场至少 62 人被捕
  • 外媒:巴基斯坦称美方谈判相关人员陆续抵达巴基斯坦;白宫官员称副总统万斯尚未启程前往伊斯兰堡

📅 今日信息:

  • 公历:2026-04-22 星期三 金牛座
  • 农历:二〇二六年三月初六
  • 公历纪念日:世界地球日、列宁诞辰纪念日
  • 下一节气:2026-05-05,立夏
  • 今年进度:30.68%(已过 112 天,剩余 252 天)

🌟 历史上的今天

  • 1870 年:列宁诞生,这位革命家后来成为苏联的奠基人,改变了 20 世纪的世界格局。
  • 1970 年:第一个地球日在美国举行,标志着现代环保运动的开端,提醒我们关爱共同的家园。

📰 今日新闻精选:

  • 春假催热 “加长版五一”:旅游产品搜索热度环比增长 364%,县域精品民宿预订量同比涨 92%
  • 官方:到 2030 年服务业总规模迈上 100 万亿元台阶,培育更多 “中国服务” 品牌
  • 31 省份一季度人均可支配收入公布:17 省居民超万元,其中上海、北京、浙江三地超 2 万元
  • 第二届世界人形机器人运动会将于 8 月在京举办,新增拔河、举重等 8 个赛项
  • 全球电力评估报告:2025 年全球太阳能激增遏制化石燃料增长,清洁能源首次超越煤电
  • 宁德时代发布第三代神行超充电池:从 10% 充电到 98% 仅需 6 分 27 秒,全球最快速度
  • 2026 年一季度 AI 应用价值榜:近四成中国网民手机装 AI,豆包月活逼近 3.5 亿稳居第一,元宝跌出前三
  • 全国首个 Pre6G 试验网在江苏南京投入运行,具备高带宽、低时延等特点,相关能力可达 5G 的 10 倍
  • 微信宣布 5 国可用微信支付:韩国、斯里兰卡、泰国、马来西亚、新加坡
  • 天猫超市发布 “超喵 1.0”:全国首个线上 AI 超市智能体落地,为品牌配备专属 “经营专家”
  • 美媒:苹果官宣换帅,特努斯将接替库克担任 CEO,长期负责硬件工程,库克称他是最佳人选
  • 日媒:日本通过内阁决议正式允许出口杀伤性武器,首批交易或向菲律宾出口二手军舰
  • 俄媒:美国提出 “丝路七国 +” 倡议,拟打造中亚南亚新地缘格局
  • 美媒:特朗普称停火到期后将轰炸伊朗;美国多名退伍老兵在国会大厦抗议对伊战争,现场至少 62 人被捕
  • 外媒:巴基斯坦称美方谈判相关人员陆续抵达巴基斯坦;白宫官员称副总统万斯尚未启程前往伊斯兰堡

📅 今日信息:

  • 公历:2026-04-22 星期三 金牛座
  • 农历:二〇二六年三月初六
  • 公历纪念日:世界地球日、列宁诞辰纪念日
  • 下一节气:2026-05-05,立夏
  • 今年进度:30.68%(已过 112 天,剩余 252 天)

🌟 历史上的今天

  • 1870 年:列宁诞生,这位革命家后来成为苏联的奠基人,改变了 20 世纪的世界格局。
  • 1970 年:第一个地球日在美国举行,标志着现代环保运动的开端,提醒我们关爱共同的家园。

lz 戴耳塞多年了,以前一直用的 3M 的,主要是白天和外出的时候戴,比较少整晚戴。
最近因为被楼下的傻逼邻居半夜吵醒了一次,导致晚上睡眠紧张,戴上耳塞会多一些安全感。

然后最近在 v 站上搜了一下关于耳塞的帖子,看到安耳悠这个品牌,发现它的价格比 3M 的要贵个几倍,我有点好奇是不是智商税,毕竟 3M 在我看来也是顶级品牌了,而且就一个耳塞还能做出什么花儿来?就买来试了一下。

戴了一晚上,很惊讶的发现耳朵居然一点感觉没有,以前戴 3M 的半夜醒来耳朵都是疼的,这个戴了一晚上都一点不疼。
而且佩戴过程中也很舒服,完全没有之前 3M 那种胀痛感,还有 3M 的戴着会有点那种沙沙的摩擦的声音,这个就不会,是一种很紧实的感觉。


说实话还真是有点小激动,没想到一个小小的耳塞居然可以做这么好。

另外大家有没有其他更牛逼的耳塞推荐,毕竟感觉下半辈子是离不开这东西了。

一个月冲了两次中转站,花了一千块,发现也没做出啥有用的东西,全给大模型打工了,有没有人做了什么有用的东西可以分享下的,看看大家都做了什么好玩意

赚钱的就算了,看着眼红,不用推广了,就看看不不赚钱的吧,平衡一下心理

今天中午拿了一袋面包回来(白色袋子没写什么面包店),说是客人给的。他说那个人声望挺高,很多人都认识。他自己还吃了 2 个,我妈也吃了半个。

我是不敢给小孩吃,不是说我怕什么的。如果换成是我,除非很是好朋友,别人给的肯定不敢吃。