Talk到URL重写,不少做Java Web的同行第一反应是把规则扔给Nginx处理,Tomcat本身似乎只是个“被动接收请求的容器”。其实从Tomcat 8开始,官方就内置了一个类似Apache httpd mod_rewrite的重写组件RewriteValve,支持绝大多数常用的重写指令,配置得当完全可以独立承担URL重写的任务。这篇文章就把RewriteValve的工作机制、配置细节、三种主流方案的选择思路以及实战中容易踩的坑一次讲清楚。

一、Tomcat重写的底层机制:RewriteValve是怎么工作的
要理解Tomcat的重写机制,先要知道Valve这个概念。Valve是Tomcat Pipeline-Valve架构中的处理单元,每个请求进入容器后会依次经过一条处理管道,Engine、Host、Context各级容器都可以挂载自己的Valve,RewriteValve就是其中一种。它的位置在请求真正到达Servlet之前,所以重写发生在URL进入Web应用逻辑之前,这也是它和应用层Filter方案最本质的区别。
RewriteValve读取的规则文件默认叫rewrite.config,语法与Apache的mod_rewrite高度兼容。请求进来后,valve会按规则文件中从上到下的顺序逐条匹配,一旦命中就执行相应的替换或跳转动作。理解“顺序匹配”这一点非常重要,后面讲避坑时会反复提到,因为很多诡异的重写行为都源于规则顺序不对。
RewriteValve有两种部署粒度:全局级和应用级。全局级配置在server.xml中,规则文件放在对应Host的目录下,对所有应用生效;应用级则是把配置写在应用的META-INF/context.xml中,规则文件放在WEB-INF目录下,只对单个应用生效。两种方式的配置写法如下。
<!-- 方式一:全局级,配置在server.xml的Host节点内 --> <Valve className="org.apache.catalina.valves.rewrite.RewriteValve" /> <!-- 方式二:应用级,配置在应用的META-INF/context.xml中 --> <lt;Context> <Valve className="org.apache.catalina.valves.rewrite.RewriteValve" /> </Context>
方式二的规则文件路径是WEB-INF/rewrite.config,方式一的规则文件路径是$CATALINA_HOME/conf/[EngineName]/[HostName]/rewrite.config,比如conf/Catalina/localhost/rewrite.config。位置放错是新手最常见的翻车原因,valve找不到规则文件时会静默失败,日志里只有一行不起眼的警告。
二、rewrite.config常用指令与实战写法
规则文件的核心指令是RewriteRule和RewriteCond。RewriteRule的完整格式是RewriteRule Pattern Substitution [flags],Pattern是匹配请求URI的正则,Substitution是替换目标,flags控制行为。RewriteCond则用于附加条件,比如只在特定Host、特定Header存在时才触发规则,多个Cond之间默认是AND关系。
先看一个最典型的场景:把不带www的域名301到带www的域名,同时强制HTTPS。这个需求用两条规则加条件就能完成,写法如下。
# 强制跳转HTTPS
RewriteCond %{HTTPS} off
RewriteRule .* https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]
# 不带www跳转到带www
RewriteCond %{HTTP_HOST} ^example\.com$ [NC]
RewriteRule ^(.*)$ https://www.ipipp.com/$1 [R=301,L]几个常用flag值得单独记住:R=301表示外部重定向且返回301状态码,不加R的规则默认是内部转发;L表示命中本条后停止后续规则匹配;NC表示忽略大小写;QSA表示保留原始查询字符串。特别注意最后这个QSA,下一节的避坑部分会重点讲它,因为它和查询字符串丢失的问题直接相关。
再来看内部重写的例子。假设老系统的链接形如/article/123.html,新系统改成了REST风格的/article?id=123,用户收藏的旧链接不能失效,就需要内部重写。
# 将 /article/123.html 内部重写为 /article?id=123 RewriteRule ^/article/(\d+)\.html$ /article?id=$1 [L]
这条规则不带R标记,浏览器地址栏不会变化,请求在容器内部被转发到新的处理路径,对用户完全透明。内部重写的性能开销远小于301重定向,因为省去了浏览器再发一次请求的往返,凡是同一应用内的路径调整都应优先用内部重写。
三、三种重写方案怎么选:RewriteValve、Filter还是Nginx
除了RewriteValve,Java生态里做URL重写还有两条常见路线:一是基于Servlet规范自己写或引入UrlRewriteFilter这样的过滤器,二是在前面架一层Nginx由它做重写再反代给Tomcat。三种方案各有明确的最适用场景,选错了会给后续维护埋雷。
RewriteValve的优势在于零代码侵入、配置集中、与容器生命周期一致,而且语法和Apache通用,运维人员上手成本低。它适合规则相对稳定、以路径跳转和域名规范化为主的场景。局限是它工作在容器层面,如果你有多台Tomcat做负载均衡,规则要在每台上重复维护,另外它对规则的调试手段比较弱,排查问题时主要靠开日志。
Filter方案的灵活性最高,可以直接写Java代码处理任意复杂的重写逻辑,比如查数据库判断旧链接映射到哪个新地址,这是规则文件做不到的。代价是和应用代码耦合,换框架或重构时规则逻辑要跟着迁移,而且所有重写逻辑都占用了应用线程的处理时间。如果重写规则涉及业务判断,Filter是唯一合理的选择;如果只是静态模式匹配,用Filter就是杀鸡用牛刀。
Nginx前置方案适合已经有Nginx做接入层的架构。重写在流量入口完成,Tomcat只处理干净的URL,职责清晰,而且Nginx的重写性能和日志能力都更成熟。缺点是多一层运维对象,小项目单独为此引入Nginx不划算。一个简单的判断标准:如果生产环境本来就有Nginx,重写就放Nginx;如果是单机Tomcat直连,优先RewriteValve;规则需要业务数据参与,就上Filter。
四、高频踩坑点与排查建议
第一个坑是查询字符串丢失。很多人写了RewriteRule ^/old/(.*)$ /new/$1 [R=301,L]之后发现跳转后的URL参数全没了。原因是替换目标里没有包含查询字符串时,默认行为取决于flag的组合:不带QSA时,如果Substitution里出现了问号,原始查询串会被丢弃;带了QSA才会追加。稳妥的写法是显式处理。
# 跳转时保留原有查询参数 RewriteRule ^/old/(.*)$ /new/$1 [R=301,QSA,L]
第二个坑是死循环重定向。典型症状是浏览器报“重定向次数过多”。原因是规则匹配了重写后的目标路径,或者HTTPS判断条件在反向代理架构下永远不成立——Nginx以HTTP方式转发给Tomcat时,%{HTTPS}在Tomcat眼里永远是off,301到HTTPS后Nginx又用HTTP转回来,无限循环。解决办法是改用Header判断。
# 反向代理场景下判断原始协议
RewriteCond %{HTTP:X-Forwarded-Proto} !https
RewriteRule .* https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]第三个坑是热部署后规则失效。war包重新部署时,如果rewrite.config打在war包的WEB-INF里,新包会正常生效;但如果规则文件是手工放在部署目录下的,重新部署会被清掉。建议要么把规则文件纳入构建产物,要么使用全局级配置,规则文件放在conf目录下不受应用部署影响。
最后是排查手段。RewriteValve本身可以输出重写日志,在Valve配置里加上相关属性即可,日志会记录每个请求命中了哪条规则、重写前后的URL是什么。遇到不生效的情况,先确认规则文件路径和文件名拼写,再确认正则是否匹配(注意URI是否带前导斜杠在不同版本有差异,建议统一按带斜杠书写并在低版本上实测),最后开日志逐条核对匹配过程,基本都能定位到原因。
Tomcat重写机制RewriteValveURL重写配置修改时间:2026-09-07 19:14:58