在排查一个老JSP项目的时候我发现接口返回的HTML页面肉眼看着不算大但用浏览器F12一看源码里从head到body之间全是一行一行的空白。再翻到JSP源文件原来是为了排版可读性留了不少空行有的空行前面还带着一串空格缩进。问题很典型JSP里的空行会被容器原样写到响应里想清除这些空白行最直接的手段就是page指令里的trimDirectiveWhitespaces属性把它设为true容器在把JSP翻译成Servlet时会把只包含空白字符的模板文本直接扔掉。这篇文章不光是介绍这个属性的用法我还会把它的原理、配置方式、能管和管不了的地方都讲清楚同时给出我在实际项目里验证过的方法和一些坑。如果你正在为JSP响应体积虚高、页面源码不够干净而发愁或者只是好奇trimDirectiveWhitespaces到底怎么生效的都可以继续往下看。1. 空白行到底是从哪来的三类来源与定位方法1.1 三类典型的空白行来源要解决空白行先得知道空白行的来源。我把平时见过的情况归纳成三类这三类的处理方式完全不同。第一类是JSP源码里为了可读性留的静态空行。比如一个列表页模型和Controller之间空两行标签和标签之间再空一行这种空行在翻译后会被原样写进响应。第二类是JSP指令之间的空白比如% page %和% taglib %周围如果留了空行同样会变成响应里的空行。第三类是JSTL标签或EL表达式在运行时产生的空白这类最隐蔽哪怕源码里没有任何多余空行c:forEach每循环一次都可能往输出里塞一个换行符。这三类空白有个非常直观的区别静态空行和指令周围的空白属于“编译期就能确认的模板文本”而JSTL标签产生的空白属于“运行时才生成的内容”。trimDirectiveWhitespaces能处理的主要是前两类第三类得靠排版或过滤器解决具体后面单独讲。第二类和第一类其实可以合并理解因为从JSP翻译器的视角看指令、标签、HTML文本之间夹着的纯空白行都是“模板文本”。模板文本这个东西在JSP里就是那些不被JSP标签和EL表达式处理的普通文字包括HTML标签、缩进、换行。它们最终都会被翻译成Servlet源码里的out.write(...)调用。你留了一个空行翻译之后就是一条写换行符的语句留了十个空行就是十条。响应体积就是这么被撑起来的。1.2 快速定位空白行来自哪里我排查这类问题时的习惯是分两步先看响应HTML源码再看JSP源码。第一步用curl把页面源码拉下来看看空行的具体位置。假设页面返回的内容里/div后面连续跟着好几个空行那就去JSP源码里找对应的/div看它后面跟着什么。如果源码里确实有空行那多半是静态模板文本如果源码里没有空行但是响应里却有那就要警惕JSTL标签和循环了。第二步可以看一下Tomcat翻译后的Java源码。在Tomcat的work目录下一般能找到类似/work/Catalina/localhost/应用名/org/apache/jsp/index_jsp.java的文件。打开后搜索out.write凡是只写了out.write(\r\n)或者out.write( \r\n)的地方就是空白行的来源。把这类write数量和JSP源码里的空行数量对照一下就能确认到底是不是静态空行。这一步很直观也是我验证配置有没有生效的常用手段。2. trimDirectiveWhitespaces为什么只对“静态空白”生效2.1 一次翻译期的“整行删除”操作trimDirectiveWhitespaces是JSP 2.1引入的page指令属性从Tomcat 6开始支持。它的作用可以用一句话概括在JSP翻译成Servlet的阶段把只包含空白字符的模板文本直接丢掉。这里有两个关键词需要注意一个是“翻译期”一个是“只包含空白字符”。翻译期意味着这个属性不是在运行时过滤响应而是在JSP编译器生成Java代码的时候做优化。JSP页面的执行过程分两段先由JSP容器把.jsp文件翻译成Servlet的Java源码再编译加载运行。模板文本在翻译期就被确定下来所以编译器能在生成Java代码时就判断这段文本是不是纯空白如果是纯空白就直接不生成对应的out.write语句。“只包含空白字符”指的是空格、Tab、换行、回车这些字符组合在一起、没有其他可见内容的一整行。举个例子h1Hello/h1 p这里上面有一个空行。/p第二行只有换行符翻译后对应的out.write就会被跳过最终输出里h1和p之间不再有空行。但注意如果某一行是p这里上面有一个空行。/p它前面带着四个空格这一行包含非空白字符所以不会被删除。也就是说trimDirectiveWhitespaces只删“整行都是空白”的模板文本不会顺手把普通行的行首缩进也一起清掉。这个细节很多人会搞混以为开了以后所有缩进都没了其实不是。2.2 为什么运行期生成的空白它管不了要理解这个边界就得对比一下JSTL标签和EL表达式。c:forEach、c:if这类标签在翻译期会被替换成Java控制逻辑标签本身不直接生成文本标签体里的模板文本则是在每次运行时通过out.write输出。trimDirectiveWhitespaces只影响翻译期生成的out.write内容它无法干预运行时循环体内部每轮迭代产生的输出。举个最常见的例子c:forEach items${list} varitem tr td${item.name}/td /tr /c:forEach就算你在页面顶部加了% page trimDirectiveWhitespacestrue %这个c:forEach每一次循环仍然会把标签体里那几行的换行和缩进完整输出一遍。500行列表就是500份换行缩进。这不是说属性没生效而是它天然管不到运行期才产生的模板文本输出。所以看一个页面该不该依赖trimDirectiveWhitespaces核心就一句话先分清这些空白是写在JSP源码里的还是由JSTL循环在运行期吐出来的。前者交给这个属性后者必须换思路。3. 落地方案page指令、web.xml和验证手段3.1 单页临时验证page指令一行搞定如果只是个别页面需要清空行最省事的办法是在JSP顶部加一条page指令% page trimDirectiveWhitespacestrue %它可以和language、contentType等属性写在同一条page指令里也可以单独写一条。单独写的好处是动一处就能恢复方便快速验证。改完之后直接刷新页面看响应源码原来的整行空白会消失。这里有个细节即便同一页里还存在JSTL循环产生的换行静态空行消失后响应源码的可读性已经好了很多。所以我的习惯是先用这个属性把静态空白清掉再判断剩下那些动态空白值不值得继续处理。如果配置之后发现没有效果优先检查三件事第一页面的JSP版本够不够新容器有没有实现JSP 2.1以上第二page指令有没有写错成trimDirectiveWhiteSpace或trimDirectiveWhitespacesTRUE这种写法第三Tomcat是不是还在用旧的翻译结果开发模式下一般会自动重编译但有些生产部署会设置JSP缓存改完配置之后最好清一下work目录再试。3.2 全站统一治理web.xml的jsp-property-group项目里几十上百个JSP不可能一个一个加page指令这种时候就走全局配置。在web.xml里加一段jsp-config?xml version1.0 encodingUTF-8? web-app xmlnshttp://xmlns.jcp.org/xml/ns/javaee xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd version3.1 jsp-config jsp-property-group url-pattern*.jsp/url-pattern trim-directive-whitespacestrue/trim-directive-whitespaces /jsp-property-group /jsp-config /web-appurl-pattern*.jsp/url-pattern表示对应用内所有JSP生效。之后新加的JSP页面不用自己写page指令也能自动获得去空行能力。这里要提醒两个配置上的坑。第一个是web.xml的schema版本trim-directive-whitespaces是JSP 2.1时代的元素建议web-app的version至少是2.5我用的是3.1更省心。第二个是元素顺序web.xml里的子元素顺序有明确约束jsp-config不能随便插到servlet前面或后面必须放在符合schema的位置否则容器启动直接报错。我把这个坑单独放到后面那章细说但配置的时候心里要先有这根弦。3.3 验证方式换行数、字节数、生成的Java源码配置全站之后不能只看一两个页面就下结论。我一般会抓三五张典型页面做前后对比比如列表页、详情页、表单页统计行数和字节数。# 修改前抓取页面 curl -s http://localhost:8080/your-app/list.jsp -o before.html wc -l before.html wc -c before.html # 修改后再次抓取 curl -s http://localhost:8080/your-app/list.jsp -o after.html wc -l after.html wc -c after.html如果页面在登录后面curl需要带上有效的Cookie或Session标识否则抓回来的是跳转页或登录页对比就没有意义。我通常会在测试环境临时造一个免认证的页面或者直接用联调环境的接口。更底层的验证方式是回到Tomcat的work目录找到list_jsp.java对比配置前后的out.write数量。配置之前源码里可能有几十条专门输出空白的out.write(\r\n)配置之后这些语句应该消失。这一步虽然繁琐但能让你彻底搞明白容器到底做了什么排查问题时特别有用。4. 它管不了的JSTL空白排版、Filter与取舍4.1 JSTL循环里逃不掉的换行在实际项目里JSTL空白往往比静态空行更恼人。静态空行数量有限但c:forEach的循环体如果排版不规范每行数据都会带出好几个换行。拿前面那个例子来说列表有500行循环体里有3个换行最终响应里就多出1500个换行符。这些换行在浏览器里几乎看不出影响但是会让响应源码变得非常难看而且对传输和调试都不友好。为什么会这样因为c:forEach标签体里的换行和缩进不是“只包含空白的一整行”它们连着tr、td这些非空字符属于包含内容的模板文本trimDirectiveWhitespaces不会去动。所以配置了全局trimDirectiveWhitespaces之后静态空行没了循环体内的换行照样还在。4.2 排版改造是最朴素也最有效的办法处理JSTL循环空白我首先推荐的是改造循环体的排版。核心思想是让标签体内部的换行尽量少或者干脆把标签贴在一起。c:forEach items${list} varitemtrtd${item.name}/td/tr/c:forEach这样写循环本身不会产生多余换行。缺点很明显代码可读性差维护的人看了想骂人。所以一般只针对那些循环次数多、性能敏感、或需要完整保留格式的页面局部使用不适合作为全项目统一规范。如果既要保持可读性又要清理循环输出就得考虑运行时HTML压缩Filter。这种Filter在响应输出之前把HTML里的连续空白替换成单个换行或直接删掉多余的空白行。好处是能同时处理静态空行和动态空行坏处是要额外引入组件、处理Content-Type判断、还要担心压缩逻辑误伤pre标签或textarea里的内容维护成本不低。我见过很多团队上了压缩Filter之后页面上原本应该保密的日志格式化输出出了偏差排错排半天。4.3 取舍原则先看影响面再决定我的取舍原则很简单如果页面的空白行主要来自JSTL循环而且页面访问量不大那排版改造的成本更低如果页面访问量很大或者响应体被当作接口数据处理那运行期压缩Filter才值得考虑。大多数后台管理系统循环体排版改一改就够了实在不行就容忍这些换行——它们对浏览器渲染几乎没有影响。5. 从字节数看性价比到底省了多少流量5.1 一个现实的数量级估算很多人纠结要不要花时间清理空白行我得给你一个真实的数字概念。一个空行如果是Unix换行符就是1字节Windows换行符则是2字节。如果空行还带了8个空格的缩进那就是9到10字节。一个50行JSP源码里空了30行、每行还缩进10个字符的页面响应里光空白部分就占了300字节左右。对单个请求来说这简直微不足道。但如果页面有循环情况就不一样了。一个500行表格循环体里每个元素多输出2个换行符那就是1000字节。这里最关键的是看有没有开gzip。如果Tomcat或前端Nginx开了gzip压缩连续空白符的压缩率极高几千字节的空白可以在压缩后变成几十字节。也就是说开了gzip之后trimDirectiveWhitespaces能省下的实际传输流量其实很有限。它更大的价值在于让响应源码干净、便于排查问题、防止某些严格的内容解析逻辑被空行干扰。那这个属性到底还有没有必要配我个人觉得有必要因为成本极低一个属性或一段web.xml配置不引入额外组件不改动页面结构顺手就让源码干净了。哪怕省下的流量不多排查问题的时候一眼能看清返回结构这个价值已经值回配置成本。5.2 收益反而明显的几类场景不过也有几类场景trimDirectiveWhitespaces的收益是实打实的。第一类是没开gzip的HTTP服务尤其是老项目里Tomcat自己扛请求、前面没有Nginx的情况空白行每一字节都是真实传输成本。第二类是页面HTML需要被拼进JSON接口返回的场景比如某些页面模板数据通过接口下发空白行会直接扩大JSON体积增加解析成本。第三类是JSP页面被用来生成邮件模板或静态文件落地的场景落地的文件每一行空行都在占用磁盘和后续的处理时间。在这些场景里静态空行的消除效果非常明显配完之后看一眼落地方案或接口响应就能感觉到差异。6. 容易踩的坑与其它方案的横向对比6.1 坑一pre标签里的空行被误删全局配置trimDirectiveWhitespaces之后最容易出问题的地方是pre标签。它本来的作用就是保留空白格式用来展示代码片段、日志、SQL等场景。如果pre内部恰好出现一行只有空白的行trimDirectiveWhitespaces会把它当普通模板文本清掉这就破坏了原本想展示的内容。我实际遇到过的情况是某个页面用pre展示多段日志日志之间用空行分隔。全局配置之后空行全部消失日志变成一坨排查时还以为模板数据出了错折腾了好久才定位到是压缩属性干的。解决方式有两种在这个页面单独加% page trimDirectiveWhitespacesfalse %覆盖全局配置或者在pre内部用HTML实体或br来表示空行避免真正的纯空白行出现。6.2 坑二web.xml元素顺序导致的容器启动失败全站配置还有一个容易踩的坑就是web.xml里jsp-config的位置。web.xml的web-app元素对子元素顺序有严格约束jsp-config不是想放哪就放哪。如果项目用的是Servlet 2.5或3.x规范jsp-config一般要放在error-page之后、security-constraint之前。很多人在原有web.xml的servlet前面直接插入jsp-config容器启动时会报类似cvc-complex-type.2.4.a: Invalid content was found starting with element jsp-config的错误。这个坑的处理方式很简单要么把jsp-config挪到合法位置要么升级web-app头部的schema版本并用IDE的校验功能检查一遍。我自己的习惯是配置完web.xml之后先重启本地容器确认能正常启动再提交代码不要等到部署环境才暴露问题。6.3 和其它方案怎么选最后把几种常见做法放一起对比一下方便你做决策。方案生效阶段能解决的空白主要代价适用场景trimDirectiveWhitespaces属性翻译期源码里的静态整行空白全局配置需注意pre等边界大多数JSP项目无脑推荐JSTL标签排版改造编译期循环体内部的换行缩进代码可读性下降循环次数多的局部页面HTML压缩Filter运行期全量HTML空白和合并空白额外组件、性能开销、脚本和pre风险响应体积敏感且无gzip场景JSP源码规范与Code Review开发期从源头减少空行依赖团队规范执行新项目或规范化改造时我的建议是先把trimDirectiveWhitespaces开了这是标准化、成本最低的解法。如果页面循环多且响应体积敏感再用排版改造或Filter作为补充。不要一上来就写一个复杂的HTML压缩Filter很多项目根本没必要还容易引入新问题。最后分享一个我给自己定的处理流程遇到JSP空白行问题先查Tomcat或前端有没有开压缩如果没开先开trimDirectiveWhitespaces把静态空行清掉再抓几个典型页面统计行数和字节数看看剩下的动态空白影响大不大最后才决定要不要改JSTL排版或上压缩Filter。按照这个顺序下来大多数项目根本不用写额外组件一个属性就能解决一大半问题。
阅读完成 · 觉得有帮助?