Tomcat 7升9,一个EncodingFilter搞崩了全站CSS

非科班野生程序员,深耕政务信息化20年。自研Java Web框架跑了十几年,一直在Tomcat 7上稳稳当当。信创改造要求升级到Tomcat 9,换了war包一启动,CSS全崩了、JS全废了、页面光秃秃只剩文字。排查下来,罪魁祸首是一个存在了十几年的EncodingFilter。这篇文章记录这次升级踩的坑。最后感谢豆包、智谱、OpenCode,决策是我做的,代码是我搓的,文字是他们总结的。


现象

Tomcat 7 → Tomcat 9,应用代码一行没改,部署启动,登录页面出来了,但:

  • CSS 没加载——样式全丢
  • JS 没执行——按钮事件不响应
  • 页面就剩一堆裸奔的 HTML 文本

打开浏览器开发者工具,Network面板里CSS和JS请求全是200,但 Content-Type 全是 application/json;charset=UTF-8

浏览器看到 application/json,当然不按CSS/JS解析了。

问题定位

框架里有个 EncodingFilter,拦截 /*,十几年前写的:

public void doFilter(ServletRequest srequest, ServletResponse sresponse, FilterChain chain)
        throws IOException, ServletException {
    HttpServletRequest request = (HttpServletRequest) srequest;
    XssHttpServletRequestWrapper xssRequest = new XssHttpServletRequestWrapper(request);
    xssRequest.setCharacterEncoding(targetEncoding);

    // 问题在这里:对所有响应都设了 Content-Type
    sresponse.setContentType("application/json;charset=UTF-8");

    // CORS 头处理...
    
    chain.doFilter(xssRequest, sresponse);
}

第54行,sresponse.setContentType("application/json;charset=UTF-8")——对所有请求,包括CSS、JS、图片,全设了 application/json

为什么在 Tomcat 7 上没问题?

因为 Tomcat 7 的 DefaultServlet 处理静态资源时,会在响应里覆盖 Content-Type。CSS文件会被设成 text/css,JS文件会被设成 application/javascript。Filter 设的值被 DefaultServlet 覆盖了,所以没问题。

Tomcat 9 行为变了。 Tomcat 9 对静态资源的处理更严格,如果 Filter 已经设了 Content-Type,DefaultServlet 不再覆盖。于是 Filter 设的 application/json 就原样返回给了浏览器。

十几年没问题的代码,容器一升级就炸了。

修复

判断URI后缀,只对动态资源设 Content-Type:

public void doFilter(ServletRequest srequest, ServletResponse sresponse, FilterChain chain)
        throws IOException, ServletException {
    HttpServletRequest request = (HttpServletRequest) srequest;
    XssHttpServletRequestWrapper xssRequest = new XssHttpServletRequestWrapper(request);
    xssRequest.setCharacterEncoding(targetEncoding);

    String uri = request.getRequestURI();
    boolean isStaticResource = uri.endsWith(".css") || uri.endsWith(".js") 
            || uri.endsWith(".png") || uri.endsWith(".jpg") 
            || uri.endsWith(".gif") || uri.endsWith(".html")
            || uri.endsWith(".woff") || uri.endsWith(".ttf") || uri.endsWith(".eot")
            || uri.endsWith(".jsp");
    
    if (!isStaticResource) {
        sresponse.setContentType("application/json;charset=UTF-8");
    }

    // CORS 头处理...
    
    chain.doFilter(xssRequest, sresponse);
}

这不只是EncodingFilter的问题

修完这个,CSS回来了。但Tomcat 7升9,坑不止这一个。趁这个机会把这次升级踩的所有坑都列出来。

坑1:web.xml 版本声明

Tomcat 7 用 Servlet 3.0,Tomcat 9 支持 Servlet 4.0。web.xml 的版本声明要改:

<!-- Tomcat 7 -->
<web-app xmlns="http://java.sun.com/xml/ns/javaee" version="3.0">

<!-- Tomcat 9 -->
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee 
                             http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
         version="4.0">

不改的话,某些新特性不生效,而且 Tomcat 9 会按旧版规范解析,行为可能不一致。

坑2:EL表达式严格模式

Tomcat 9 的 EL 解析更严格。以前 #{}${} 混用不出错,Tomcat 9 直接报异常。

框架里 JSP 页面如果同时用了 #{}(延迟表达式)和 ${}(立即表达式),升级后会抛 ELException

修法:要么统一用 ${},要么在 web.xml 里加:

<jsp-config>
    <jsp-property-group>
        <url-pattern>*.jsp</url-pattern>
        <el-ignored>false</el-ignored>
    </jsp-property-group>
</jsp-config>

坑3:Cookie SameSite 属性

Tomcat 9 对 Cookie 的处理更安全了,默认不再允许跨站发送 Cookie。政务系统里如果前端和后端不在同一个域(比如前端走Nginx代理),Session Cookie 可能被浏览器拦截。

JSESSIONID 发不出去,登录就失效。

修法:在 context.xml 里配置:

<Context>
    <CookieProcessor className="org.apache.tomcat.util.http.Rfc6265CookieProcessor"
                      sameSiteCookies="lax" />
</Context>

坑4:SetCharacterEncodingFilter 的顺序

web.xml 里我后来加了一个 Tomcat 自带的 SetCharacterEncodingFilter

<filter>
    <filter-name>setCharacterEncodingFilter</filter-name>
    <filter-class>org.apache.catalina.filters.SetCharacterEncodingFilter</filter-class>
</filter>
<filter-mapping>
    <filter-name>setCharacterEncodingFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

注意它在 web.xml 的最底部。Tomcat 7 对 filter-mapping 的顺序不太敏感,Tomcat 9 严格按声明顺序执行。如果这个 Filter 在 EncodingFilter 之前执行,编码可能被覆盖。

经验:多个 Filter 拦截 /* 时,顺序就是命运。 web.xml 里谁先声明谁先执行,Tomcat 9 不会帮你调整。

坑5:静态资源缓存 Filter 的交互

框架里还有个 CacheFilter,给图片、CSS设缓存头:

<filter-mapping>
    <filter-name>cache</filter-name>
    <url-pattern>*.css</url-pattern>
</filter-mapping>

Tomcat 7 时,CacheFilter → EncodingFilter → DefaultServlet,各管各的没问题。Tomcat 9 时,EncodingFilter 如果对静态资源也设了 Content-Type,会和 CacheFilter 的缓存行为冲突——浏览器缓存了 Content-Type: application/json 的CSS文件,后续请求直接从缓存读错误内容。

修了 EncodingFilter 之后,CacheFilter 的缓存内容也需要清一遍。

经验教训

1. Filter 不应该对静态资源动手动脚。 当年写 EncodingFilter 时根本没考虑静态资源,反正 Tomcat 7 会"兜底"覆盖。这个"兜底"在 Tomcat 9 上没了。

2. 容器升级不能只换war包。 Tomcat 7→9 跨了两个大版本,Servlet 规范从 3.0 到 4.0,很多"约定"变成了"规范",以前靠容器"容错"的代码全部暴露。

3. 跑了十几年的代码不代表没问题。 只是一直没遇到触发条件。换个容器、换个JDK、换个运行环境,隐性的坑全冒出来。

4. 升级前先跑一遍 Filter 链的梳理。 哪些 Filter 拦截 /*、每个 Filter 对 Response 做了什么、对静态资源有没有副作用——列清楚再升级。

小结

Tomcat 7升9,不是换个war包的事。十几年的老框架,在旧容器上"没问题的代码",在新容器上可能是定时炸弹。EncodingFilter 对静态资源设了 application/json,Tomcat 7 默默帮你覆盖了,Tomcat 9 不帮了,全站CSS就崩了。

修起来不难,难的是知道要修


老项目容器升级,大家还踩过什么坑?评论区交流。

标签:#Tomcat #容器升级 #Filter #Servlet #EncodingFilter #CSS失效 #老项目维护 #政务信息化

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐