4.2 部署方式与目录结构 本节摘要:Web 应用是迷宫里的住户,本节讲住户的三种搬家方式——解压目录、WAR 包、管理端远程部署——各自的适用场景与行为差异;逐项解释标准目录树中每个位置的作用;并剖析 autoDeploy 与热部署的触发机制与生产代价。学完你能独立完成一次规范的发布,并知道每种发布方式在日志里长什么样。 4.1 的路由系统认识得再清楚,也得有住户可指。本节补上"应用怎么进来":部署方式的选型、目录结构的约定、以及 autoDeploy 这个默认开启却常被误解的机制——第 7 章排查部署故障时,本节的行为描述就是判据。
本节摘要:Web 应用是迷宫里的住户,本节讲住户的三种搬家方式——解压目录、WAR 包、管理端远程部署——各自的适用场景与行为差异;逐项解释标准目录树中每个位置的作用;并剖析 autoDeploy 与热部署的触发机制与生产代价。学完你能独立完成一次规范的发布,并知道每种发布方式在日志里长什么样。
4.1 的路由系统认识得再清楚,也得有住户可指。本节补上"应用怎么进来":部署方式的选型、目录结构的约定、以及 autoDeploy 这个默认开启却常被误解的机制——第 7 章排查部署故障时,本节的行为描述就是判据。
| 方式 | 操作 | 适用场景 | 特点 |
|---|---|---|---|
| 解压目录 | 把应用目录直接放进 appBase | 开发调试、需要改文件 | 免打包、改动即时可见 |
| WAR 包 | 把打包文件放进 appBase | 标准发布 | 原子更新、与构建流水线对接 |
| 管理端部署 | 经 manager 应用上传或脚本触发 | 远程发布、自动化 | 不登录机器即可发布 |
三种方式的共同底层:Tomcat 扫描 appBase 目录(默认 webapps),发现新住户就为它建一个 Context,上下文路径由目录名或包名决定——文件名就是应用的门牌号,这条约定与 4.1 的最长前缀匹配直接挂钩。
每个 Web 应用的内部结构由 Servlet 规范约定,一个位置放错,轻则资源读不到,重则应用起不来。

用命令把最小应用装配出来,比看图更扎实。六条命令造一个能跑的应用骨架。
# 造一个最小应用 demo 并放进部署目录 base=webapps/demo mkdir -p $base/WEB-INF/classes $base/WEB-INF/lib $base/assets echo '<html>demo home</html>' > $base/index.html cat > $base/WEB-INF/web.xml <<'EOF' <?xml version="1.0" encoding="UTF-8"?> <web-app xmlns="https://jakarta.ee/xml/ns/jakartaee" version="6.0"> <welcome-file-list> <welcome-file>index.html</welcome-file> </welcome-file-list> </web-app> EOF tail -n 4 logs/catalina.out
信息 [ContainerBackgroundProcessor] org.apache.catalina.core.StandardContext[/demo] 部署描述符配置完成 开始处理注解 信息 [main] org.apache.catalina.startup.HostConfig.deployDirectory 把 webapps 中目录 [demo] 的应用部署了 [326] 毫秒
目录放进去几秒内,日志出现部署记录——这就是 autoDeploy 在后台扫描的效果,无需重启。
Host 标签上的 autoDeploy 默认开启,行为是:后台线程周期性扫描 appBase,新目录或新包部署之,更新的包重新部署之,删除的目录卸载之。听起来很美,生产上却要三思。
代价一:内存碎片。反复热部署会让旧应用的类加载器来不及释放,代数累积后出现内存增长,最终要靠定时重启收场——这是"为什么每周要重启一次 Tomcat"的经典答案。代价二:检测有延迟。扫描间隔默认数秒,发布系统若不确认日志就切流量,会有一段新旧混跑。代价三:并发不安全。大包替换瞬间,正巧进来的请求可能撞上半卸载状态。
规范的发布姿势是走管理端或脚本做"停、换、起"的受控流程,用 manager 应用的文本接口一条命令完成。
# 经 manager 的文本接口远程部署一个 WAR 包 curl -u deploy:secret -T target/order-app.war \ 'http://localhost:8080/manager/text/deploy?path=/order&update=true'
OK - Deployed application at context path [/order]
返回 OK 才算发布成功,随后再用 list 接口确认运行状态。这套接口可以嵌进流水线,比"拷文件加祈祷"可靠得多。安全提醒:manager 应用的账号与角色在 5.2 与第 7 章还会出现,弱口令加公网可达的 manager 是经典入侵入口,务必收到内网并强口令。
💡 关键直觉:部署方式的选型其实是"谁负责原子性"——目录直拷没有原子性,WAR 替换有文件级原子性,管理端部署有事务级确认。生产发布至少要到第二档,自动化到位就上第三档。
发布动作之前过一遍清单,八成的事故能在上线前拦下。
| 检查项 | 合格标准 |
|---|---|
| 包内结构 | WEB-INF 与 web.xml 齐全,静态资源在应用根 |
| 上下文路径 | 包名与目标路径一致,根应用包名为 ROOT |
| 环境差异 | 数据源与外部地址按环境区分,不带测试值 |
| 依赖完整性 | 应用库目录齐备,与容器公共库无版本冲突 |
| 发布确认 | 见到部署日志或管理端返回 OK 再切流量 |
| 回滚预案 | 上一版包仍在服务器上,回滚步骤演练过 |
另一个进阶做法是把应用放在 appBase 之外:webapps 目录只留给容器自带内容,业务应用用独立的 Context 片段指向外部路径。好处是升级容器目录时应用原地不动,迁移成本趋近于零——多实例运维(2.1 的 HOME 与 BASE 分离)时这个布局尤其顺手。
<!-- conf 目录下的独立片段文件:应用住在 appBase 之外 --> <Context docBase="/data/apps/order-app" path="/order"/>
注意 docBase 指向应用本体、path 声明访问路径,两者解耦后改名不搬家、搬家不改名。片段文件的文件名习惯上与应用对应,放在 conf 的对应虚拟主机子目录里即可生效。
住户搬进来了,下一节看它的两份登记表——web.xml 声明"我是谁、我有哪些组件",context.xml 声明"我需要什么资源"。