惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

S
Schneier on Security
F
Fortinet All Blogs
博客园_首页
The GitHub Blog
The GitHub Blog
V
Visual Studio Blog
D
DataBreaches.Net
aimingoo的专栏
aimingoo的专栏
爱范儿
爱范儿
B
Blog
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
N
Netflix TechBlog - Medium
阮一峰的网络日志
阮一峰的网络日志
P
Proofpoint News Feed
D
Docker
Engineering at Meta
Engineering at Meta
大猫的无限游戏
大猫的无限游戏
The Cloudflare Blog
罗磊的独立博客
云风的 BLOG
云风的 BLOG
Microsoft Azure Blog
Microsoft Azure Blog
T
The Exploit Database - CXSecurity.com
博客园 - 三生石上(FineUI控件)
量子位
The Last Watchdog
The Last Watchdog
MyScale Blog
MyScale Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
T
The Blog of Author Tim Ferriss
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
小众软件
小众软件
Cloudbric
Cloudbric
博客园 - 司徒正美
H
Help Net Security
人人都是产品经理
人人都是产品经理
Application and Cybersecurity Blog
Application and Cybersecurity Blog
L
LangChain Blog
Latest news
Latest news
M
MIT News - Artificial intelligence
T
Threat Research - Cisco Blogs
博客园 - Franky
S
Security Affairs
W
WeLiveSecurity
F
Full Disclosure
Know Your Adversary
Know Your Adversary
Google DeepMind News
Google DeepMind News
The Hacker News
The Hacker News
Cyberwarzone
Cyberwarzone
美团技术团队
PCI Perspectives
PCI Perspectives
C
Check Point Blog
Spread Privacy
Spread Privacy

博客园_首页

Plist 二进制格式 Milvus 和 PGVector,哪个更好? OpenClaw 已过时?在 VS Code 中运行 Hermes Agent! 第30篇文章:一个大三计科生的自白 Manim如何在数学公式中完美显示中文? Docker 部署 RocketMQ 5 并发编程核心概念辨析 C#事务处理最佳实践:别再让“主表存了、明细丢了”的破事发生 CLI 是什么?为什么大厂突然集体卷命令行? 【从0到1构建一个ClaudeAgent】协作-自主Agent UIImageView 设置图片不生效的原因排查 最小二乘问题详解20:无先验约束下的增量式SFM自由网平差 痞子衡嵌入式:大话双核i.MXRT1180之XIP应用里借助MU实现可靠Flash IAP的方法 AI Chat 封装, SemanticKerne.AiProvider.Unified 已发布 Windows下右键编辑js文件无法打开记事本——在注册表中使用环境变量 在后台服务中使用 Scoped 服务,为什么总是报错? H200 安装驱动并使用sglang启动模型 wireshark 抓包Trap上报告警内容 我用 AI 辅助开发了一系列小工具(2):图片压缩工具 [A Primer On MC and CC] 2.1 Memory Consistency 1 - 指令重排序和 SC 模型 Oracle数据库SCN推进技术详解与实践指南 玩转控件:封装个带图片的Label控件 Claude Code 4.7 真正该升级的不是模型,而是你的工作流 前端小白一句话,AI 帮我做了个颜值拉满的桌面媒体播放器。当代码不再是门槛,一句话编程就是现实。 5. WorkBuddy: 小龙虾的灵魂三件套,让你的小龙虾不只是工具 SQLite 分片方案实战:三种分片策略的深度对比 告别简陋 UI!一款基于 Fluent Design 和基于 WinUI 的开源免费、现代化的 Avalonia UI 控件库 关于二进制排列组合枚举的总结 AI开发-python-LangGraph框架(3-27-LangGraph从零实现大模型智能决策工作流) ElasticSearch主分片和副本分片概念详解 【002】HTTPS 粗解:证书、TLS 握手与对后端配置的影响 Hermes Agent 一周暴涨五万 Star,但我劝你别急着追 明明连接的是Redis的DB0,为什么能查到DB3的数据? 【从0到1构建一个ClaudeAgent】协作-Agent团队 熟悉电子元器件之后,电子小白下一步该怎么走? MAF快速入门(23)通过C#类定义Skills .NET 高级开发 | 手写一个对象映射框架 FastAPI数据库ORM怎么选?我肝了三个Demo后,终于不再纠结了 mysqldump 参数拾遗:在遗忘与铭记之间 C# .NET 周刊|2026年3月5期 Claude code入门 - 陈彦斌 一文学习入门 ThingsBoard 开源物联网平台 GitHub 热门项目 | 2026年04月16日 如何为GIT设置全局勾子,为每次提交追加信息 Number.isFinite和isFinite与isNaN()和Number.isNaN的区别 PortSwigger SQL注入LAB2 推荐一个测试人必备的Skills,从功能到性能全搞定(附详细实操和安装下载方式) 筑基期:掌握Odoo基础核心知识点02(Odoo XML 开发方式详解) GLM模型这么火,咱们用vllm也咧一个呗! 深入理解 AbortController:从底层原理到跨语言设计哲学 字符串学习笔记 多租户系统框架的基础模块设计和分析设计 Apache SeaTunnel Zeta 为什么能做到“又快又稳”? AI开发-python-LangGraph框架(3-26-LangGraph基本概念及第一个简单样例) Vue 3 组件通信,别只会用 Props 和 Emits 了,这几个狠活儿你得看看 ElasticSearch7.X版本配置密码 用Manim实现动态交点计算--从一个动点问题说起 团结引擎+Addressable+Instant Game打包抖音小游戏 function call 实战:让 LLM 自动判断 pod 异常、调用日志工具并完成故障分析 bubseek —— 让 Agent 的足迹,变成团队的洞察 通过 C# 读取并导出 PDF 书签 如何用 GitHub Actions 实现 Steam 自动化发布 【从0到1构建一个ClaudeAgent】并发-后台任务 .NET 高级开发 | 定制 ASP.NET Core 框架 电子小白:什么是运算放大器(运放) zero2Agent:面向大厂面试的 Agent 工程教程,从概念到生产的完整学习路线 堆上的ORW HC32F460 USB CDC通信异常:非对齐访问异常排查 20260413-Hyperbridge 攻击事件:发生在默克尔山上的验证绕过 那些喊着AI 要淘汰你的人,正在靠你的焦虑赚大钱! 深度学习进阶(八)Swin Transformer 最小二乘问题详解19:带先验约束的增量式SFM优化与实现 SnapTranslate 3.0 正式发布:全局划词翻译 + 完整英语学习闭环,一站式搞定查词、记词、复习 工作的意义、工作的困难认知再思考 .NET + AI 进阶实战:基于类的技能开发 - 打造可治理的 Agent 能力模块 【从0到1构建一个ClaudeAgent】规划与协调-技能 上周热点回顾(4.6-4.12) 电子小白的工具三件套:面包板、杜邦线、万能板 单表五亿数据的查询优化 | Mysql、StarRocks 2. WorkBuddy:从“我是谁”到“帮我干活” C# 如何减少代码运行时间:7 个实战技巧 基于HelixToolkit.SharpDX 渲染3D模型 - 笺上知微 从零开始的双臂具身VLA起源及现阶段发展综述 - SkyXZ 记对 xonsh shell 的使用, 脚本编写, 迁移及调优 - pluvium27 受够了Vibe Coding的失控?换个起点,让AI事半功倍 从开始配置漏洞环境到漏洞复现流程 - 難しい 关于10年工作经验的程序员对OpenClaw的实战经验分享以及看法 - 虚无境 Any metadata 的内存布局 C# .NET 周刊|2026年3月2期 - InCerry 我帮你测过了,测试圈排名第二的 Skill 依然很牛逼 Skill Discovery | 无监督技能发现的经典工作总结 - MoonOut 上下文工程是什么?过时了么?一文讲明白! - 一枫说码 开了 TUN 模式还是直连?90% 的人都踩过这个坑 AScript扩展多种脚本语言 - rockey627 AI 学习笔记:Agent 的记忆机制 你能被装进一个文件里吗?——7 万人把同事"蒸馏"成了 AI - 我没有三颗心脏 Claude Code 通关手册(七):给 AI 装上技能包——Skills 完全指南 - 暮色之狐 在浏览器中快速编辑代码:VSCode Web 集成实践 - Newbe36524 蒸馏自己 skill?基于 Deepseek 的蒸馏器,丐版蒸馏方式,简单便捷 - To_Carpe_Diem Spring AI Aliababa和AgentScope,哪个更好? - 苏三说技术
Tomcat Container容器之Engine:StandardEngine
程序员Seven · 2026-05-06 · via 博客园_首页

理解思路

  • 第一:抓住StandardEngine整体类依赖结构来理解

  • 第二:结合server.xml中Engine配置来理解

见下文具体阐述。

  • 第三:结合Engine Config官方配置文档

http://tomcat.apache.org/tomcat-9.0-doc/config/engine.html

Engine接口设计

这看Engine.java接口前,先要看下相关属性

  • 支持设置的属性列表
属性 描述
backgroundProcessorDelay 此值表示在此引擎及其子容器(包括所有Host和Context)上调用backgroundProcess方法之间的延迟(以秒为单位)。如果子容器的延迟值不为负(则表示它们正在使用自己的处理线程),则不会调用它们。将此值设置为正值将导致产生线程。等待指定的时间后,线程将在此引擎及其所有子容器上调用backgroundProcess方法。如果未指定,则此属性的默认值为10,表示10秒的延迟。
className 使用的Java类名称。此类必须实现org.apache.catalina.Engine接口。如果未指定,将使用标准值(定义如下)。
defaultHost 默认的主机名,它标识Host将处理针对主机名此服务器上的请求,但在此配置文件中没有配置。此名称必须与嵌套在name 其中的Host元素之一的属性匹配。
jvmRoute 必须在负载平衡方案中使用的标识符才能启用会话亲缘关系。标识符(在参与集群的所有Tomcat服务器之间必须是唯一的)将附加到生成的会话标识符上,因此允许前端代理始终将特定会话转发到同一Tomcat实例。注意,jvmRoute也可以使用jvmRoutesystem属性设置 。属性中的jvmRoute set<Engine>将覆盖任何jvmRoute系统属性。
name 此引擎的逻辑名称,用于日志和错误消息。在同一台Server中使用多个Service元素时 ,必须为每个引擎分配一个唯一的名称。
startStopThreads 该引擎将用来并行启动子Host元素的线程数。特殊值0将导致使用该值 Runtime.getRuntime().availableProcessors()。Runtime.getRuntime().availableProcessors() + value除非小于1,否则将使用负值, 在这种情况下将使用1个线程。如果未指定,将使用默认值1。如果使用了1个线程,那么ExecutorService将使用当前线程,而不是使用。
  • Engine的接口设计

这里你会发现,如下接口中包含上述defaultHost和jvmRoute属性设置;同时还有Service,因为Engine的上层是service。

/**
 * An <b>Engine</b> is a Container that represents the entire Catalina servlet
 * engine.  It is useful in the following types of scenarios:
 * <ul>
 * <li>You wish to use Interceptors that see every single request processed
 *     by the entire engine.
 * <li>You wish to run Catalina in with a standalone HTTP connector, but still
 *     want support for multiple virtual hosts.
 * </ul>
 * In general, you would not use an Engine when deploying Catalina connected
 * to a web server (such as Apache), because the Connector will have
 * utilized the web server's facilities to determine which Context (or
 * perhaps even which Wrapper) should be utilized to process this request.
 * <p>
 * The child containers attached to an Engine are generally implementations
 * of Host (representing a virtual host) or Context (representing individual
 * an individual servlet context), depending upon the Engine implementation.
 * <p>
 * If used, an Engine is always the top level Container in a Catalina
 * hierarchy. Therefore, the implementation's <code>setParent()</code> method
 * should throw <code>IllegalArgumentException</code>.
 *
 * @author Craig R. McClanahan
 */
public interface Engine extends Container {

    /**
     * @return the default host name for this Engine.
     */
    public String getDefaultHost();


    /**
     * Set the default hostname for this Engine.
     *
     * @param defaultHost The new default host
     */
    public void setDefaultHost(String defaultHost);


    /**
     * @return the JvmRouteId for this engine.
     */
    public String getJvmRoute();


    /**
     * Set the JvmRouteId for this engine.
     *
     * @param jvmRouteId the (new) JVM Route ID. Each Engine within a cluster
     *        must have a unique JVM Route ID.
     */
    public void setJvmRoute(String jvmRouteId);


    /**
     * @return the <code>Service</code> with which we are associated (if any).
     */
    public Service getService();


    /**
     * Set the <code>Service</code> with which we are associated (if any).
     *
     * @param service The service that owns this Engine
     */
    public void setService(Service service);
}
  • 其它属性支持都包含在我们上文分析的ContainerBase中
/**
  * The processor delay for this component.
  */
protected int backgroundProcessorDelay = -1;
/**
  * The number of threads available to process start and stop events for any
  * children associated with this container.
  */
private int startStopThreads = 1;

...

Engine接口实现:StandardEngine

接口中简单方法实现

上述接口里面的defaultHost, JvmRoute, service 很简单

/**
  * Return the default host.
  */
@Override
public String getDefaultHost() {
    return defaultHost;
}


/**
  * Set the default host.
  *
  * @param host The new default host
  */
@Override
public void setDefaultHost(String host) {

    String oldDefaultHost = this.defaultHost;
    if (host == null) {
        this.defaultHost = null;
    } else {
        this.defaultHost = host.toLowerCase(Locale.ENGLISH);
    }
    if (getState().isAvailable()) {
        service.getMapper().setDefaultHostName(host);
    }
    support.firePropertyChange("defaultHost", oldDefaultHost,
                                this.defaultHost);

}


/**
  * Set the cluster-wide unique identifier for this Engine.
  * This value is only useful in a load-balancing scenario.
  * <p>
  * This property should not be changed once it is set.
  */
@Override
public void setJvmRoute(String routeId) {
    jvmRouteId = routeId;
}


/**
  * Retrieve the cluster-wide unique identifier for this Engine.
  * This value is only useful in a load-balancing scenario.
  */
@Override
public String getJvmRoute() {
    return jvmRouteId;
}

/**
  * Return the <code>Service</code> with which we are associated (if any).
  */
@Override
public Service getService() {
    return this.service;
}


/**
  * Set the <code>Service</code> with which we are associated (if any).
  *
  * @param service The service that owns this Engine
  */
@Override
public void setService(Service service) {
    this.service = service;
}

child, parent

addChild重载方法,限制只能添加Host作为子容器;

setParent直接抛出异常,因为Engine接口中已经包含了setService方法作为它的上层,而Engine的上层没有容器的概念。

/**
  * Add a child Container, only if the proposed child is an implementation
  * of Host.
  *
  * @param child Child container to be added
  */
@Override
public void addChild(Container child) {

    if (!(child instanceof Host))
        throw new IllegalArgumentException
            (sm.getString("standardEngine.notHost"));
    super.addChild(child);

}


/**
  * Disallow any attempt to set a parent for this Container, since an
  * Engine is supposed to be at the top of the Container hierarchy.
  *
  * @param container Proposed parent Container
  */
@Override
public void setParent(Container container) {

    throw new IllegalArgumentException
        (sm.getString("standardEngine.notParent"));

}

Lifecycle的模板方法

无非就是调用上文中我们介绍ContainerBase中的方法

@Override
protected void initInternal() throws LifecycleException {
    // Ensure that a Realm is present before any attempt is made to start
    // one. This will create the default NullRealm if necessary.
    getRealm();
    super.initInternal();
}


/**
  * Start this component and implement the requirements
  * of {@link org.apache.catalina.util.LifecycleBase#startInternal()}.
  *
  * @exception LifecycleException if this component detects a fatal error
  *  that prevents this component from being used
  */
@Override
protected synchronized void startInternal() throws LifecycleException {

    // Log our server identification information
    if (log.isInfoEnabled()) {
        log.info(sm.getString("standardEngine.start", ServerInfo.getServerInfo()));
    }

    // Standard container startup
    super.startInternal();
}

LogAccess

这里需要补充下之前没有介绍的日志访问,这里介绍下。

运行Web服务器时,正常生成的输出文件之一是访问日志,该访问日志以标准格式为服务器处理的每个请求生成一行信息。Catalina包括一个可选的Valve实现,该实现可以创建与Web服务器创建的标准格式相同的访问日志,也可以创建任意数量的自定义格式。

需要先看下xml配置; 您可以通过嵌套如下所示的Valve元素,要求Catalina为Engine, Host或Context处理的所有请求创建访问日志:

<Engine name="Standalone" ...>
  ...
  <Valve className="org.apache.catalina.valves.AccessLogValve"
         prefix="catalina_access_log" suffix=".txt"
         pattern="common"/>
  ...
</Engine>

好了看下具体的实现,使用适配器模式获取AccessLog类型的Valve:

适配器模式看这里:结构型 - 适配器(Adapter)

@Override
public AccessLog getAccessLog() {

    if (accessLogScanComplete) {
        return accessLog;
    }

    AccessLogAdapter adapter = null;
    Valve valves[] = getPipeline().getValves();
    for (Valve valve : valves) {
        if (valve instanceof AccessLog) { // 看这里
            if (adapter == null) {
                adapter = new AccessLogAdapter((AccessLog) valve);
            } else {
                adapter.add((AccessLog) valve);
            }
        }
    }
    if (adapter != null) {
        accessLog = adapter;
    }
    accessLogScanComplete = true;
    return accessLog;
}

AccessLog(日志记录器)主要的作用就是记录日志,这个记录的方法就是logAccess()方法

/**
  * Override the default implementation. If no access log is defined for the
  * Engine, look for one in the Engine's default host and then the default
  * host's ROOT context. If still none is found, return the default NoOp
  * access log.
  */
@Override
public void logAccess(Request request, Response response, long time,
        boolean useDefault) {

    boolean logged = false;

     // 如果有accessLog,则记录日志
    if (getAccessLog() != null) {
        accessLog.log(request, response, time);
        logged = true;
    }

    // 没找到且使用useDefault,表示从下层容器中获取accessLog
    if (!logged && useDefault) {
        AccessLog newDefaultAccessLog = defaultAccessLog.get();
        if (newDefaultAccessLog == null) {
            // If we reached this point, this Engine can't have an AccessLog
            // Look in the defaultHost
            Host host = (Host) findChild(getDefaultHost()); // 如果没有默认的accessLog,则获取默认Host的accessLog
            Context context = null;
            if (host != null && host.getState().isAvailable()) {
                newDefaultAccessLog = host.getAccessLog();

                if (newDefaultAccessLog != null) {
                    if (defaultAccessLog.compareAndSet(null,
                            newDefaultAccessLog)) {
                        AccessLogListener l = new AccessLogListener(this,
                                host, null);
                        l.install(); // 注册AccessLog监听器至当前Engine
                    }
                } else {
                    // Try the ROOT context of default host
                    context = (Context) host.findChild(""); // 如果仍然没有找到,则获取默认host的ROOT Context的accessLog
                    if (context != null &&
                            context.getState().isAvailable()) {
                        newDefaultAccessLog = context.getAccessLog();
                        if (newDefaultAccessLog != null) {
                            if (defaultAccessLog.compareAndSet(null,
                                    newDefaultAccessLog)) {
                                AccessLogListener l = new AccessLogListener(
                                        this, null, context);
                                l.install();
                            }
                        }
                    }
                }
            }

            if (newDefaultAccessLog == null) { 
                newDefaultAccessLog = new NoopAccessLog(); // 这个其实是一个空模式,以便采用统一方式调用(不用判空了)
                if (defaultAccessLog.compareAndSet(null,
                        newDefaultAccessLog)) {
                    AccessLogListener l = new AccessLogListener(this, host,
                            context);
                    l.install();
                }
            }
        }

        // 最后记录日志,(上面最后有空模式实现,所以可以直接调用,不用判空)
        newDefaultAccessLog.log(request, response, time);
    }
}

其中涉及的相关内部类如下:

protected static final class NoopAccessLog implements AccessLog {

    @Override
    public void log(Request request, Response response, long time) {
        // NOOP
    }

    @Override
    public void setRequestAttributesEnabled(
            boolean requestAttributesEnabled) {
        // NOOP

    }

    @Override
    public boolean getRequestAttributesEnabled() {
        // NOOP
        return false;
    }
}

protected static final class AccessLogListener
        implements PropertyChangeListener, LifecycleListener,
        ContainerListener {

    private final StandardEngine engine;
    private final Host host;
    private final Context context;
    private volatile boolean disabled = false;

    public AccessLogListener(StandardEngine engine, Host host,
            Context context) {
        this.engine = engine;
        this.host = host;
        this.context = context;
    }

    public void install() {
        engine.addPropertyChangeListener(this);
        if (host != null) { // 同时注册至host和context
            host.addContainerListener(this);
            host.addLifecycleListener(this);
        }
        if (context != null) {
            context.addLifecycleListener(this);
        }
    }

    private void uninstall() {
        disabled = true;
        if (context != null) {
            context.removeLifecycleListener(this);
        }
        if (host != null) {
            host.removeLifecycleListener(this);
            host.removeContainerListener(this);
        }
        engine.removePropertyChangeListener(this);
    }

    @Override
    public void lifecycleEvent(LifecycleEvent event) {
        if (disabled) return;

        String type = event.getType();
        if (Lifecycle.AFTER_START_EVENT.equals(type) ||
                Lifecycle.BEFORE_STOP_EVENT.equals(type) ||
                Lifecycle.BEFORE_DESTROY_EVENT.equals(type)) {
            // Container is being started/stopped/removed
            // Force re-calculation and disable listener since it won't
            // be re-used
            engine.defaultAccessLog.set(null);
            uninstall();
        }
    }

    @Override
    public void propertyChange(PropertyChangeEvent evt) {
        if (disabled) return;
        if ("defaultHost".equals(evt.getPropertyName())) {
            // Force re-calculation and disable listener since it won't
            // be re-used
            engine.defaultAccessLog.set(null);
            uninstall();
        }
    }

    @Override
    public void containerEvent(ContainerEvent event) {
        // Only useful for hosts
        if (disabled) return;
        if (Container.ADD_CHILD_EVENT.equals(event.getType())) {
            Context context = (Context) event.getData();
            if (context.getPath().isEmpty()) {
                // Force re-calculation and disable listener since it won't
                // be re-used
                engine.defaultAccessLog.set(null);
                uninstall();
            }
        }
    }
}

JMX相关

之前已经有过相关介绍,这里不再介绍相关方法,只列出相关方法:

@Override
protected String getObjectNameKeyProperties() {
    return "type=Engine";
}


@Override
protected String getDomainInternal() {
    return getName();
}