本节摘要:自定义标签的本质是"翻译期替换":编译器按描述文件的契约,把页面上的标签整体替换为"实例化处理器、设置属性、调用执行方法"的 Java 代码。标签有两代模型——传统接口家族把生命周期拆成多个回调方法,简化模型把一切收进单方法;另有标签文件提供零 Java 的铸造方式。本站是车间的基础课:懂机制,才能读懂老站里每一代标签的出身与脾气。
第 5 章用 c:forEach 替掉了循环脚本,你也许隐约好奇过:容器凭什么认识这个 c: 前缀的标签?答案就是本章的全部地基——标签在翻译期被整体替换。编译器遇到一个自定义标签时,按标签库描述文件找到对应处理器类,然后在翻译产物里生成一串调用代码:
// 页面上的 <my:highlight color="gold">热销</my:highlight> // 翻译产物里变成了大致这样一串: HighlightTag handler = new HighlightTag(); // 实例化处理器 handler.setJspContext(pageContext); // 注入页面上下文 handler.setColor("gold"); // 逐个设置属性 handler.setJspBody(fragment); // 递标签体 handler.doTag(); // 执行
看清这段产物代码,标签的三条"怪规矩"立刻有了解释。其一,属性必须有配套的写入方法——翻译产物要调 setColor,处理器里就得有。其二,属性类型声明错误在编译期就暴露——翻译期就要确定调用哪个方法。其三,处理器为每次标签调用单独创建——产物代码里明晃晃的 new,所以处理器内的实例变量只在本次调用内存续期,不必担心跨请求共享,这正是它和 3.1 节声明区变量的本质区别。
把四方协作画成图,这张图贯穿本章三站。

老站的标签有明显的代际差。第一代模型把生命周期拆成多个回调:无体的标签实现开始与结束两个方法;需要重复处理体的再加上"体后方法",返回值决定要不要再来一轮;要完全控制体内容的还有初始化与注入方法。配套的支撑类替你垫了默认实现,但一个简单的循环标签仍要十几行样板,返回值常量的语义更是劝退无数人。青梧书肆最老的一批标签就是这代出身,读它们的诀窍是先认生命周期方法,再顺藤摸瓜。
第二代模型在规范 2.0 时代登场,把一切收进一个执行方法:设属性走写入方法,体内容拿到的是一个"可执行片段",想捕获就喂给字符串缓冲,想直接输出就原地触发。一个高亮标签十余行收工:
public class HighlightTag extends SimpleTagSupport { private String color; public void setColor(String color) { this.color = color; } @Override public void doTag() throws JspException, IOException { StringWriter buf = new StringWriter(); getJspBody().invoke(buf); // 执行体内容并捕获 getJspContext().getOut().print( "<span class='hl-" + color + "'>" + buf + "</span>"); } }
两代模型的取舍值得一张表说清:
| 维度 | 传统接口家族 | 简化单方法模型 |
|---|---|---|
| 生命周期 | 多个回调方法拼装 | 单个执行方法包办 |
| 体内容处理 | 缓冲区手工管理 | 片段对象一次到位 |
| 循环渲染 | 体后方法返回值控制 | 片段想执行几遍就几遍 |
| 代码量 | 简单标签也要十几行 | 十行以内常见 |
| 维护建议 | 老站存量,读懂即可 | 新标签一律用它 |
还有第三条路:标签文件。它本质是一个特殊命名的页面文件,用属性指令声明入参,用体执行动作输出体内容,放在约定的目录下就能被页面引入使用:
<%-- 铸造:徽章.tag --%> <%@ tag description="状态徽章" %> <%@ attribute name="level" required="true" %> <span class="badge badge-${level}"><jsp:doBody/></span> <%-- 使用:页面里三行引入,一处声明 --%> <my:badge level="warn">库存紧张</my:badge>
结构组合型的构件——卡片、表单项、徽章——用标签文件铸最快,前端同学自己就能维护;需要查数据、跑业务规则的构件才值得写 Java 处理器。判据就一条:逻辑重用 Java,结构重用标签文件。
背景:改造商品列表页时发现一个自定义标签 my:bookRow,页面里用得很好,但没人说得清它干了什么。打开处理器,发现它继承的是老接口家族的支撑类,实现了开始、体后、结束三个方法。
操作:按代际读法拆解。开始方法里校验属性并初始化行号;体后方法里判断"还没循环到最后一行"就返回再执行一轮,同时把行数据沉进页面上下文供体内取用;结束方法里清理状态。对照本章的生命周期图,十几行代码的意图全部对上。
结果:确认它等价于"带状态循环"标签,行为与 c:forEach 加行数据注入一致。改造决策:页面保留调用不动,处理器内部重构成单方法模型,外部行为零变化。
解读:老标签的维护心法是"先按代读意图,再决定动不动"。很多祖传标签的对外契约(属性名、体语义)已被几十个页面依赖,重写内部实现远比替换调用安全。改造老代码时,保契约、换内核是比推倒重来高明得多的手法。
变式:如果祖传标签的意图实在读不明白,先给它写几个"标本页面"固定住现有输出,再动手术——标本在手,重构前后逐字节比对,安全感完全不同。这个手法不止用于标签,老站任何黑盒构件的改造都适用。
💡 关键直觉:标签机制的全部秘密就是那句"翻译期替换"。读不懂某个标签行为时,别盯着页面猜,去想翻译产物里那串调用代码会长什么样——产物即真相。
机制课还有两处细节,读老标签时经常用到。其一是嵌套标签的上下文传递:子标签能沿处理器链找到父标签实例,这既是能力也是约束——子标签脱离父标签单独使用时的行为,必须在处理器里显式校验并给出人话报错,而不是任由空指针糊脸。青梧书肆的车间规矩是:任何可嵌套的标签,处理器入口先验父级,验不过就抛出带使用示例的异常。其二是异常路径下的状态清理:标签执行中途抛异常时,已经沉进页面作用域的变量、已经写进输出流的半截内容,都不会自动回滚。所以车间代码评审有一条固定检查:doTag 里 setAttribute 与输出语句之后、return 之前的每一条路径,都要么不会抛异常,要么抛了也无所谓。老标签里没做这套清理的,接手时记入整改清单,别指望它们自我痊愈。
机制课毕,下一站处理那根"牵线":标签库描述文件。它是标签故障的头号案发地,逐项排查清单马上奉上。