本节摘要:标签库描述文件(TLD)是标签体系的中枢契约:统一资源标识让页面找到库,标签声明让库找到处理器类,属性声明与体内容类型划定接口边界。上一站看清了四方协作,本站把这根牵线拆开逐股检查——实战里标签类故障的绝大多数,病根都在这份文件上。本站交付一份可背的排查清单与一次完整故障复盘。
先看全貌。一份典型的描述文件分两个层次:库级元数据加一组标签声明:
<taglib version="2.0"> <tlib-version>1.0</tlib-version> <short-name>mytags</short-name> <uri>qingwu-taglib</uri> <tag> <name>bookRow</name> <tag-class>com.qingwu.tags.BookRowTag</tag-class> <body-content>scriptless</body-content> <attribute> <name>book</name> <required>true</required> <rtexprvalue>true</rtexprvalue> <type>com.qingwu.domain.Book</type> </attribute> </tag> </taglib>
这份文件里每个元素都在回答一个具体问题。uri 回答"页面怎么找到这个库"——页面指令里的 uri 属性与它精确匹配,匹配成功,前缀生效。注意它是逻辑标识不是网址,取名随意但全站唯一。name 与 tag-class 回答"这个标签由谁执行"——页面上写 my:bookRow,容器就在本库里查 name 等于 bookRow 的声明,找到类名去实例化。body-content 回答"标签体内允许放什么"。attribute 组回答"调用时能传什么、必传吗、能传动态值吗、什么类型"。
四个关键声明的语义密度最高,单独列清:
| 声明 | 管什么 | 填错的典型症状 |
|---|---|---|
| uri | 页面指令与库的匹配键 | 报"找不到标签库",页面编译不过 |
| body-content | 体内容类型边界 | 声明为空却写了内容,编译报错 |
| required | 属性是否必传 | 少传属性运行时抛异常 |
| rtexprvalue | 属性能否收动态值 | 设了假却传 EL,翻译期直接拒绝 |
body-content 的三个常用取值值得记牢:empty 表示无体,标签只能自闭;scriptless 表示体里可以放模板文本、EL 与嵌套标签,但禁止脚本片段——新标签一律选它,既够用又守住去脚本化成果;tagdependent 表示体内容原样交给处理器自行解释,容器不做任何加工,特殊场景才用。至于 rtexprvalue 这个名字古怪的老开关,记一句话就够:要收 EL 值的属性必须开,只收字面量的属性必须关——比如安全敏感的属性强制只收字面量,还能顺手防一层注入。
契约写得再好,容器找不着也是白搭。发现机制只有两条正路。其一,把描述文件放在工程的私有区目录下(习惯上建个子目录归拢),页面指令里用相对路径直接引用——自己的库这么放,直观可控。其二,把标签库连同描述文件打成分发包,描述文件放进分发包的元数据目录,工程把包丢进依赖目录后容器自动扫描注册,页面只需写 uri——跨工程复用的库这么放,即插即用。老站点两条路都见过,排查第一步永远是确认这份文件到底在哪个世界。

背景:同事铸好一个订单徽章标签,处理器编译通过,描述文件语法正确,页面也按文档写了引入指令。可页面跑起来,标签原样印在页面上,徽章效果完全没有——不报错、不生效,像个影子。
操作:按路线图走。"原样印出"是典型症状:容器根本没把它认成标签,当成普通模板文本输出了——问题必在匹配环节。核对页面指令的 uri 与描述文件:指令里写的是 qingwu-tags,描述文件里声明的是 qingwu-taglib,一个字母之差,匹配失败。容器对认不出的前缀的处理方式不是报错,而是当文本放行——于是有了这场沉默失踪。
结果:统一 uri 后页面立即生效。
解读:这个案例暴露了标签体系的一个人性化设计及其代价——容错是沉默的。为兼容历史遗留写法,容器对认不出的标签选择放行而非报错,代价就是这类故障没有任何异常可循。排查心法由此而来:只要"标签原样显示",直接跳过一切中间环节,先查 uri 与前缀的匹配。
变式:同族故障还有两种变体。标签生效但属性值是空的——查 rtexprvalue 开关,关着的时候传 EL 会被翻译期拒绝或求值为字面量;标签在开发环境正常、部署后失效——查描述文件是否真的进了部署产物,增量部署漏带配置文件是老站运维的经典事故,解压部署包看一眼只要半分钟。
⚠️ 常见坑:多人协作时 uri 各写各的,最后库里同一批标签冒出两三个不同写法的引入指令。约束 uri 与版本号一起管理,改版本必须同步全站搜索替换——这活儿值得交给构建脚本,人肉必漏。
契约文件还有两个工程化管理的话题。其一,版本化:库里标签的属性契约一旦被页面依赖,就成了公共接口——改属性名、改必填性都属于破坏性变更。车间的做法是在 uri 里带大版本号(小版本修复不动 uri),破坏性变更换新 uri 旧版并存,让页面按自己的节奏迁移,而不是一夜全站崩塌。其二,多库共存:站点同时引用标准库、自研库、第三方库时,前缀冲突是常事——同一页面里两个库都注册了 if 这种名字,谁先被声明谁生效,另一个静默失效。排查口诀:前缀冲突看 taglib 声明的先后,看不准就把本地前缀改成独有名字。青梧书肆最终约定自研库前缀一律带 qw 前缀(qw:table、qw:pager),与标准库天然隔离,冲突从此绝迹。命名纪律虽然朴素,却是多库共存最省钱的解。
读别人的契约文件有一套心法,让老库快速显形。先看库级三件:版本号判断新旧、统一标识判断引用方式、描述文本判断定位——三行读完对整个库有基本盘。再扫标签名录:只看每个标签的名字与体类型,不进属性细节,三十秒画出一库的能力地图;名字即语义,名字含混的标签八成也是含混的实现。第三步才挑重点标签进属性细节:看必填项推使用门槛,看动态值开关推数据流向,看类型声明推处理器的严谨程度。这套由面到点的心法,半小时能读完一个中型自研库。青梧书肆交接时,接手同事照此读完车间三组标签的契约,然后在标本页面上各跑一遍,当天下午就能独立改标签——契约文件是给"读"的,不是给"猜"的,心法对了它就是最好的文档。
契约与排查都掌握了,下一站正式下车间:铸一组带属性、带标签体、父子协作的复合标签。