WADE老Java Web工程接手:Ant构建与HTML组件化CRUD实践
简介:WADE开发文档面向使用WADE框架进行企业级Java Web开发的中初级开发者,围绕框架入门、项目搭建与版本升级改造提供系统说明。文档以单一doc文件收录,压缩包约3.24MB,内容聚焦quickstart生成空项目、build.properties项目参数配置与WADELIB包路径设置,并梳理新建项目、开发改造、新增数据等常见开发场景,对2.52至2.59等版本的兼容性调整与建议用法也有集中归纳。已有973人学习,适合需要快速理解框架结构、减少升级迁移踩坑的读者。其价值在于把分散的配置项、组件用法和版本差异整理成可检索的参考,帮助读者建立从项目初始化到功能改造的整体认知,并作为日常开发中的查阅手册。
1. 从一份 WADE 开发文档说起:Ant 构建 + HTML 组件化,这套老骨架今天还怎么接手
前段时间有人接手一个跑了十几年的 Java Web 工程,仓库里没有 pom.xml,只有 build 目录下的 build.xml 和十几行 build.properties;前端不是 JSP 也不是 Vue,而是一堆 HTML 文件,标签上挂着jwcid="@wade:PageRedirect"这类非标准属性,onclick 里调的是redirectTo('examples.basic.VipcustEdit', null, parameters)。翻遍仓库,能对上号的就一份 WADE 开发文档。这套东西的价值不在于新,而在于它把「新建工程 → 增删改查 → 批量 → Excel 导入」的每一步都钉死在固定模板上,照着文档走,一个下午能出一套能跑的业务页面;反过来,不看文档硬改,一个Buttom拼写错误就能让你对着空白页排查半天。这篇把这份文档里真正能落地的部分拆开讲:谁在什么场景下用、参数怎么填、哪几个地方我吃过亏。
2. 用 quickstart 起一个新工程:build.properties 里每个参数都要对上
WADE 没有依赖管理器的概念,新建工程的方式很原始:拿一份叫 quickstart 的模板工程,跑一次 Ant 目标,在 build 目录下生成一套全新的空项目骨架。这个动作看起来简单,但它背后是一次批量文本替换 + 目录重命名,参数填错一个,生成出来的工程要么编译不过,要么带着 quickstart 的残留页面。
2.1 为什么是 ant newproject,而不是复制粘贴一份模板
很多人第一反应是直接cp -r quickstart newprj,把目录名改掉就完事。这个做法在 WADE 里会翻车。因为 quickstart 里的类名、包路径、page.application 配置、sidebar 菜单、甚至 war 包名,都是以quickstart / Quickstart / QUICKSTART三种大小写形态散落在几十个文件里的。手改漏一处,编译时找不到类还是小事,最坑的是运行期某个页面跳转 404,你得顺着配置文件一层层挖。
ant newproject干的就是这件事:读取 build.properties 里声明的三组旧名和三组新名,按大小写形态做全量替换,再把目录结构复制过去。常见做法是把新项目的三种形态一次想清楚再动手,比如新系统叫 custmgr,那OLD_NAME1/2/3就是quickstart/Quickstart/QUICKSTART,NEW_NAME1/2/3就对应custmgr/Custmgr/CUSTMGR。三组必须形态一致,小写对的小写、驼峰对的驼峰、全大写对的全大写,交叉填错会产生Custmgr和CUSTMGR混用的半成品工程。
第一步永远是先进 build 目录,把所有参数确认一遍再执行:
# 进入构建目录,所有 ant 目标都在这里执行 cd build # 先确认 build.properties 里的新旧名称已按三种大小写形态填好 # 再执行生成,命令不带参数,读取的就是当前目录的 build.properties ant newproject # 生成完成后回上级目录,做一次全量编译验证 cd .. ant allant newproject没有交互式确认,参数错了它照跑不误,生成完你才发现新工程名叫Newquickstart。所以我的习惯是先cp build.properties build.properties.bak再改,出问题直接回滚,比重跑一次省事。
2.2 build.properties 逐行拆:路径、编码、版本三块
这份配置文件的参数可以分成三类:路径类决定 Ant 去哪找源码和依赖,编码类决定编译产物里的字符集,名称类决定生成工程叫什么。下表按原文给的配置项整理,重点看第三列的说明。
| 参数 | 示例值 | 含义与注意点 |
|---|---|---|
PROVINCE_NAME | common | 编译时的省代码。开发期统一用 common 走公用代码,打包部署时换成实际省份代码,这是 WADE 做多省份定制的方式 |
PROJECT_NAME | wade-quickstart | 项目名称,最终产出的 jar / war 文件名都以它为准,改这里等于改交付物名字 |
PROJECT_HOME | .. | 项目根目录,..表示 build 目录的上级,一般不用动 |
WADELIB_HOME | ../../../wadelib/wadelib | WADELIB 包所在路径,框架核心 jar 从这里的 lib 目录取 |
build | ${PROJECT_HOME}/build | 构建脚本目录 |
config | ${PROJECT_HOME}/etc | 配置文件目录,ehcache、import 定义都在这下面 |
source | ${PROJECT_HOME}/src | Java 源码目录 |
classes | ${PROJECT_HOME}/web/WEB-INF/classes | 编译输出目录,注意它在 web 下面,不是 target |
lib | ${WADELIB_HOME}/web/WEB-INF/lib | 依赖 jar 目录,指向 WADELIB 而非项目自身 |
web | ${PROJECT_HOME}/web | 静态资源与页面目录 |
APPLANG | GB2312 | 编译编码。老工程基本都是 GB2312,改 UTF-8 要连带改页面 meta、文件实际编码、IDE 设置,不建议半路动 |
APPLANG这一项值得单独说两句。新手看到 GB2312 第一反应是「乱码,改成 UTF-8」,改完之后中文注释编译报错、页面输出问号。原因是这套模板里的 HTML 文件本身就是 GB2312 存盘的,配置和文件编码必须一致。要真改,是一次全量转码工程,不是改一行配置能解决的事。
pdf/lib这两项都指向 WADELIB 而不是项目内目录,这意味着 WADE 的依赖是「共享 lib」模式,多个应用共用一个 wadelib 包。好处是升级框架只改一处,坏处是不同应用对同一个 jar 版本有要求时会打架。这也是为什么下面要讲目录结构约束。
2.3 WADELIB 和应用目录的相对位置,决定 build.xml 要不要改
文档里有一句很关键的提醒:一般建议 wadelib 和应用目录在同一级。原因是 build.xml 里引入build_wadelib.xml用的是相对路径,默认按同级目录算。一旦你的实际部署把 wadelib 放到了别处,Ant 会报找不到文件,报错信息通常是 import 行号,不太直观。
<!-- build/build.xml 中引入框架构建脚本的那一行,默认按同级目录推算 --> <import file="../../wadelib/build/build_wadelib.xml"/> <!-- 若 wadelib 与应用不在同一级,把相对路径改成实际层级 --> <import file="../../../wadelib/build/build_wadelib.xml"/>判断规则很简单:从 build 目录出发,往上走到能和 wadelib 目录平级的那一层,../的个数就是路径前缀。改完别忘了WADELIB_HOME也要同步,这两个值必须指向同一个 wadelib,否则编译期用的框架 jar 和构建脚本引用的框架版本可能对不上,出现「编译通过、运行期 ClassNotFound」的玄学问题。
2.4 ant all 和 ant allwar 的分工,以及 ant-contrib 这个前置
开发期和发布期走两个目标,这一点文档写得很明确:开发用ant all,测试打包发布用ant allwar。前者只编译到 classes 目录,改完代码刷新页面就生效;后者会打成 war,包含完整的目录结构和依赖。
ant allwar有一个额外前置:需要 ant-contrib.jar。这个 jar 提供 Ant 原生的 if/else、循环等逻辑控制,WADE 的打包脚本里用到了。没有它,执行 allwar 会直接报taskdef class net.sf.antcontrib.logic.IfTask cannot be found。
# 开发期:只编译,产物进 web/WEB-INF/classes ant all # 发布期:打 war 包,执行前确认 ant-contrib.jar 已放到 Ant 的 lib 目录 # 或者通过 -lib 参数临时指定 jar 所在目录 ant -lib ./lib allwar把 ant-contrib.jar 放进 Ant 安装目录的 lib 下是全局生效的做法,团队里每个人都要做一次;-lib方式只在当次命令有效,适合 CI 环境或者不想动全局配置的场景。我一般在项目 build 目录下放一份,写进一个 build.sh 里固定住,新人拉代码跑脚本就行,省掉「我这能打包你那不能」的来回确认。
新建工程的完整动作到这儿就闭环了:改 build.properties → ant newproject → 删 quickstart 残留配置 → 编译验证。第三步的清理最容易被跳过,page.application 和 sidebar.html 里还留着 quickstart 示例页面的注册项和菜单项,不删的话菜单上会多出一堆点进去报错的死链。判断方法就是全局搜quickstart,把配置项删干净,页面文件留着当参考实现不影响运行。
3. 从 HTML 到 Bean 的三层落地:新增、修改、查看的骨架怎么搭
WADE 的业务页面是严格的四层分工:HTML 声明组件和事件、Page 类接请求做编排、Bean 类写业务和 DAO 调用、DAO 落库。这套分层本身不新鲜,新鲜的是每一层之间靠什么串起来——HTML 靠jwcid属性绑定组件,Page 靠PageData拿参数和上下文,Bean 靠IData传数据。搞不清这三个东西的边界,写出来的代码会到处取不到值。
3.1 HTML 层:jwcid 绑定和 redirectTo 的参数含义
页面上做跳转不用<a href>,而是声明一个组件:
<!-- 列表页的「新增」按钮:点击后跳到编辑页,不带参数 --> <a jwcid="@wade:PageRedirect" source="ognl:condition" value="新增" class="e_bLinkAdd" onclick="redirectTo('examples.basic.VipcustEdit', null, parameters)"/> <!-- 列表页的「查看/修改」链接:把整行数据作为参数带过去 --> <a jwcid="@wade:PageRedirect" source="ognl:info" columns="VIP_ID" value="ognl:info.VIP_ID" onclick="redirectTo('examples.basic.VipcustEdit', 'queryVipcust', parameters)"/>jwcid是组件声明,@wade:PageRedirect表示这是个跳转组件。redirectTo是框架注入的全局函数,三个参数依次是目标页面全名、目标页面上的监听方法名、当前页面待传递的参数集合。第二个参数传 null 表示只跳页面不调方法;传queryVipcust表示跳过去之后立刻在目标 Page 上执行这个方法,用查出来的数据填充表单。
source和columns配合使用,决定哪些字段会变成 URL 参数带过去。source="ognl:info"指向列表当前行对象,columns="VIP_ID"说明只取主键列。常见错误是source指向了一个循环里已经失效的变量,跳过去参数是空的,再往后就是「您要查看的记录已不存在」这种提示。
3.2 Page 层:PageData 是这个世界的入口
Page 类的每个 public 方法对应一个页面事件,参数固定是IRequestCycle。方法体第一步永远是拿PageData:
public void createVipcust(IRequestCycle cycle) throws Exception { PageData pd = getPageData(); ExamplesBean bean = new ExamplesBean(); // 表单数据在 pd.getData() 里,整包交给 Bean 处理,Page 不写业务 String vip_id = bean.createVipcust(pd, pd.getData()); // 把主键传下去,下一个页面能从 PageData 里直接取出 pd.setTransfer("VIP_ID", vip_id); // 四组数组按位置一一对应:页面、监听方法、按钮文案 redirectToMsg("VIP 客户资料新增成功,选择【继续新增】跳转到新增页面,选择【查看详情】跳转到详情页面", new String[] { getPageName(), getPageName() }, new String[] { null, "queryVipcust" }, new String[] { "继续新增", "查看详情" }); }pd.getData()返回的是表单提交上来的IData,键名就是 HTML 里组件的 name。pd.setTransfer(key, value)把值放进跨页面传递区,接收方用pd.getParameter("VIP_ID")取。pd.getContext()拿的是会话上下文,员工号、部门、业务区这些身份信息都从这里出,不要自己去读 session。
redirectToMsg的四个参数必须长度一致,第 n 个页面对应第 n 个监听方法和第 n 个按钮文案。少写一个元素,运行时数组越界,报错信息不会告诉你哪一行配错了。写这个方法的时候我一般先写页面数组,再照着数量补齐另外两个,避免错位。
3.3 Bean 层:数据填充的固定套路和容易漏的字段
Bean 里做的事很直白:取序列、补全审计字段、插库。
public String createVipcust(PageData pd, IData data) throws Exception { ExamplesDAO dao = new ExamplesDAO(pd); // 主键不靠自增,统一走数据库序列,避免并发下重号 String vip_id = dao.getSequence("SEQ_VIP_ID"); // 表单里客户标识和用户标识同源,这里做一次覆盖,保证两边一致 data.put("USER_ID", data.get("USECUST_ID")); data.put("CUST_ID", data.get("USECUST_ID")); data.put("CUST_NAME", data.get("USECUST_NAME")); data.put("VIP_ID", vip_id); data.put("REMOVE_TAG", ExampleFactory.VIP_REMOVETAG_VALID); data.put("NET_TYPE_CODE", ExampleFactory.VIP_NETTYPE_DEFAULT); // 审计字段全部来自上下文,不接受前端传入,防止被伪造 data.put("IN_STAFF_ID", pd.getContext().getStaffId()); data.put("IN_DEPART_ID", pd.getContext().getDeptId()); data.put("CITY_CODE", pd.getContext().getCityId()); data.put("EPARCHY_CODE", pd.getContext().getEpachyId()); data.put("JOIN_DATE", common.getSysTime()); data.put("IN_DATE", common.getSysTime()); dao.insert("TF_F_CUST_VIP", data); return vip_id; }规律是:业务字段从前端 data 里取,主键从序列取,状态位和网别用常量类兜底,人员部门地区和时间一律从pd.getContext()和common.getSysTime()取。最后这条是安全边界,前端传什么员工号都不认。新建页面最容易漏的是REMOVE_TAG这个有效标记,漏了之后记录插进去了但列表查不出来,因为默认查询条件带了有效标记过滤。
文档里还有一句注释值得记住:所有数据都以文本方式导入数据集,时间也直接写成yyyy-MM-dd HH:mm:ss形式的字符串。也就是说这一层不做类型转换,转换由 DAO 按目标表字段类型处理。好处是代码里不用到处写 Date 格式化,坏处是格式不合法时错误会抛在 DAO 里,堆栈不指向你的业务代码。
3.4 修改和删除:为什么必须带 REMOVE_TAG 一起做条件
新增是插入,改和删才是真正考验设计的地方。文档专门提醒了一个场景:同一个页面被打开多次,或者多人同时操作,在你提交之前别人已经改过甚至删掉了这条记录。这时候如果只按主键更新,会把别人的修改直接覆盖掉,而且没有任何提示。
WADE 的做法是把「能识别记录当前状态的关键列」一起放进更新条件:
public boolean updateVipcust(PageData pd, IData data, String[] values) throws Exception { ExamplesDAO dao = new ExamplesDAO(pd); // values 是 {vip_id, remove_tag},两个值都匹配上才更新 // 返回 false 表示没有行被影响,说明记录已被改动或已注销 return dao.save("TF_F_CUST_VIP", data, new String[] { "VIP_ID", "REMOVE_TAG" }, values); }Page 层拿到 false 就直接common.error("修改失败,信息在此操作前已经被操作过"),把冲突暴露给用户而不是静默吞掉。这就是最朴素的乐观并发控制,用状态列当版本号使。删除同理,dao.delete("TF_F_CUST_VIP", data, new String[] { "VIP_ID", "REMOVE_TAG" }),注销过的记录删不掉,符合业务预期。
这里还有一个必须分清的差别:save和update。update是整行覆盖,data 里没带的字段会被置空;save是先按主键查出原记录,再把 data 里的字段覆盖上去,其余字段保持原值。做表单编辑页面时,页面上只提交了部分字段,用update会把没在表单里出现的列全部清空——这是我在老系统里见过最多的一类数据丢失。判断标准很简单:data 是否包含该表的全部业务字段,包含才用update换性能,否则老老实实save。
4. 批量与导入:DatasetList、executeBatch 和 ExcelParser 的实际用法
单条增删改查照着模板抄就行,真正拉开工作量差距的是批量和导入。这两个功能在 WADE 里各有两套写法:批量有「通用批量」和「自定义批量」,导入有「标准模板导入」和「带错误回写导入」。选错写法,代码量差三倍。
4.1 通用批量:DatasetList 攒数据,一次落库
批量新增的核心是不在循环里调 insert,而是把整批数据攒成一个IDataset再交给 DAO:
public void createVipcusts(PageData pd, IDataset dataset) throws Exception { ExamplesDAO dao = new ExamplesDAO(pd); for (int i = 0; i < dataset.size(); i++) { IData data = (IData) dataset.get(i); // 逐条补全主键和审计字段,规则与单条新增完全一致 data.put("VIP_ID", dao.getSequence("SEQ_VIP_ID")); data.put("REMOVE_TAG", ExampleFactory.VIP_REMOVETAG_VALID); data.put("IN_STAFF_ID", pd.getContext().getStaffId()); data.put("IN_DATE", common.getSysTime()); } // 一次性批量插入,减少网络往返 dao.insert("TF_F_CUST_VIP", dataset); } public void deleteVipcusts(PageData pd, String[] vips) throws Exception { ExamplesDAO dao = new ExamplesDAO(pd); IDataset dataset = new DatasetList(); for (int i = 0; i < vips.length; i++) { IData data = new DataMap(); data.put("VIP_ID", vips[i]); dataset.add(data); } // 批量删除,条件列由 DAO 内部按主键处理 dao.delete("TF_F_CUST_VIP", dataset); }注意dao.insert和dao.delete对IDataset的重载是框架封装好的,内部会切成 JDBC 的 batch 执行。主键仍然要逐条取序列,别想着循环外取一次然后自增——序列值不连续,手工自增在并发下会撞号。
HTML 侧批量入口用PageSubmit而不是PageRedirect,因为要在当前页触发提交:
<!-- 勾选多行后点删除,先弹确认框,确认后提交到 deleteVipcusts --> <a jwcid="@wade:PageSubmit" value="删除" listener="ognl:listeners.deleteVipcusts" desc="VIP 客户资料" class="e_bLinkDelete" onclick="return queryBox(this, 'vips');"/>queryBox(this, 'vips')返回 false 时整个提交被拦下,这是前端唯一一道确认。desc会拼进确认框文案,写清楚操作对象,别留空。
4.2 自定义批量:通用 CRUD 覆盖不了时用 executeBatch 手工绑参
有些批量操作不是简单的 insert/update/delete,比如「按客户标识批量改名称和类型,且每行改的值都不一样」。这时候通用方法没法表达,得自己拼 SQL 和参数:
public void batchUpdateVipcusts(IData param) throws Exception { // 前端把多行数据以数组形式提交,这里按列拆成三个平行数组 String[] vip_ids = common.getValues(param.get("VIP_ID")); String[] cust_names = common.getValues(param.get("CUST_NAME")); String[] cust_types = common.getValues(param.get("CUST_TYPE")); String sql = "update TF_F_VIP_CUST set CUST_NAME = ?, CUST_TYPE = ? where VIP_ID = ?"; // 每个 Parameter 对应一行,add 的顺序必须和 SQL 里 ? 的顺序严格一致 Parameter[] params = new Parameter[vip_ids.length]; for (int i = 0; i < params.length; i++) { params[i] = new Parameter(); params[i].add(cust_names[i]); params[i].add(cust_types[i]); params[i].add(vip_ids[i]); } executeBatch(sql, params); }这段代码有两个必须盯住的地方。第一,Parameter.add的顺序就是占位符顺序,cust_names和cust_types写反了类型对不上,报的错是类型转换,不指向具体行号,只能靠日志逐行核。第二,三个数组的长度必须一致,common.getValues拆出来的数组长度取决于前端提交的结构,某一行少传一个字段就会长度不齐,后面的行全部错位。稳妥做法是在循环前加一次长度断言,长度不等直接抛业务异常。
executeBatch是框架提供的方法,不走 DAO 的通用封装,事务边界由 Page 层控制,中途失败整批回滚。
4.3 Excel 导入:两份 XML 配置,一份管进一份管错
导入是 WADE 里配置最重的一块,需要两个 XML:一份定义导入模板的列格式和校验规则,一份定义导出失败数据时的列格式。
导入配置示例:
<!-- etc/import/examples/basic/VipcustList.xml,定义 Excel 列与字段的映射 --> <sheet desc="VIP 客户资料"> <header isshow="true" height="300"> <!-- type: 1字符串 2数字 3时间;nullable: 是否必填;maxsize: 最大长度 --> <cell name="USECUST_ID" desc="客户标识" type="2" align="1" width="4000" nullable="no" maxsize="16"/> <cell name="USECUST_NAME" desc="客户名称" type="1" align="1" width="6000" nullable="no" maxsize="100"/> </header> </sheet>失败回写配置示例:
<!-- etc/export/examples/basic/VipcustImport.xml,把导入失败的行原样导出 --> <sheet name="VIP 客户资料" desc="日程信息"> <header isshow="true" height="300"> <cell name="SCHE_SUBJECT" desc="日程主题" type="1" align="1" width="6000"/> <!-- 固定加一列错误原因,值由业务代码写入 IMPORT_ERROR --> <cell name="IMPORT_ERROR" desc="导入错误" type="1" align="1" width="10000"/> </header> </sheet>失败回写这份配置有一条铁律:所有列都设成type="1",不做任何格式转换。原因文档讲得很清楚,如果某列设成时间类型并指定了format="yyyy-MM-dd HH:mm",用户原本填的是2007-03-01,导出时会变成2007-03-01 00:00,原始数据被改写,用户拿着这份错误文件改完再导一次,数据就对不上了。导出配置里也不要设format,保持原样。
4.4 导入页面的三个必写项和 Page 层的错误汇总
导入页面比普通表单多几个硬性要求,缺一个就读不到文件:
<!-- 1. form 必须有 enctype,否则服务器拿不到文件流 --> <form jwcid="@Form" enctype="multipart/form-data"> <!-- 2. 文件域 name 要与后台 getUploadFile 的参数一致 --> <input type="file" name="FILE_PATH"/> <!-- 3. 提交按钮走 PageSubmit,监听导入方法 --> <a href="#" jwcid="@LinkSubmit" listener="ognl:listeners.importVipcusts" value="导入" desc="日程信息"/> <!-- 4. 模板下载走 attach,file_name 是下载显示名,file_path 是服务器实际路径 --> <a href="attach?file_name=日程批量导入模板.xls&file_path=/templet/examples/basic/VipcustTemplet.xls" target="printframe">下载模板</a> </form>后台的处理流程是「解析 → 逐行入库 → 收集失败行 → 生成错误文件 → 汇总提示」:
public void importVipcusts(IRequestCycle cycle) throws Exception { PageData pd = getPageData(); // importExcel 返回 IDataset 数组,取第 0 个即解析结果 IDataset dataset = ExcelParser.importExcel(pd, "examples/basic/VipcustList.xml", pd.getUploadFile("FILE_PATH").getStream())[0]; ExamplesBean bean = new ExamplesBean(); // 业务校验在 Bean 里做,返回失败行集合 IDataset failds = bean.importVipcusts(pd, dataset); if (failds.size() != 0) { // 把失败行按错误模板导出,返回文件 id String file_id = ExcelParser.writeExcelByImport(pd, "examples/basic/VipcustImport.xml", new IDataset[] { failds }); // 用文件 id 生成下载链接,拼进提示文案 redirectToMsgByPopup("VIP 客户资料批量导入情况:共导入" + dataset.size() + "条,成功" + (dataset.size() - failds.size()) + "条,失败" + failds.size() + "条,请点[" + ExcelParser.getFailedLinkByImport(file_id, "日程批量导入失败列表.xls") + "]下载导入失败文件"); } else { redirectToMsgByPopup("VIP 客户资料批量导入成功,按【关闭】关闭窗口回主页面"); } }Bean 里逐行处理时,成功行进succds,失败行除了原数据还要补两个字段:IMPORT_RESULT置 false、IMPORT_ERROR写具体原因。这两个字段就是错误文件里能看出「为什么失败」的唯一来源,写清楚「客户标识超长」比写「导入失败」有用得多。
还有一条容易被忽略:文档要求导入页面必须加verifyAll / confirmForm / confirmInfo这组标准验证方法,否则后台取不到数据。这不是可选项,是框架在解析上传文件前的前置钩子,少了它getUploadFile返回 null。
5. 版本升级的六处翻车点排查:从 2.5 到 2.59 逐条对
WADE 的「开发改造」章节本质上是一份升级 checklist。每个版本列出来的建议点,都是「原来能跑但建议换写法」或者「不换就会报错」两类。前者可以缓,后者必须改。下面这六条是我在实际工程里真碰过问题的,按「现象 → 原因 → 解决」写。
5.1 现象:页面底部整块报错,控制台提示找不到 Buttom 页面
这是一条纯拼写引发的血案。2.52 之前,底部页面文件叫web/Buttom.html和view/Buttom.java,正确拼写应该是Bottom。某次升级后框架内部按正确拼写去找,旧的错误文件名就找不到了。
原因很明确:框架侧的引用路径改了,但应用里的文件名和page.application里的注册项还是旧拼写。解决要三处一起改——文件名改成Bottom.html/Bottom.java,page.application里对应的页面注册项改成新名字,然后全局搜一遍Buttom确认没有残留引用。只改文件名不改配置,报错会从「找不到文件」变成「页面未注册」,换个错误继续。
5.2 现象:页面加载后初始化脚本不执行,下拉框没数据
2.5 之前常用<script>init();</script>这种方式在页面末尾手写初始化调用。升级后这类写法失效,页面看着正常但联动逻辑全不跑。
原因是框架改了初始脚本的调用机制:每个页面载入时自动调用一个按页面名生成的方法,比如页面全名examples.basic.VipcustEdit对应initVipcustEidt()。框架按约定找方法,找不到就静默忽略,不报错。所以现象是「什么都没发生」,最难查。
解决分两步:先删掉 HTML 里手写的init()调用;再把初始化逻辑挪到按约定命名的方法里。要自定义方法名也可以,用@wade:Head的initScript参数指定,但方法必须存在且签名正确,否则一样静默跳过。排查这类问题的土办法是在初始化方法第一行打日志,日志不出来就说明方法没被调上,回头查命名。
5.3 现象:用 document.getElementById 禁用日期组件,只灰掉了输入框,日历图标还能点
这条踩过的人最多。老代码里到处是document.getElementById(name).disabled = true,对普通输入框没问题,对日期组件就翻车。
原因是 WADE 的很多组件不是单一 DOM 节点。日期组件实际由一个文本框加一个图标组成,getElementById只能拿到其中之一,改它的 disabled 只是灰掉半个组件。文档明确要求改用框架封装的方法:getElement(name)、getElements(name)、hidden(name|obj)、disabled(name|obj)。这几个方法知道组件的真实结构,会对整个组件生效。
解决的迁移成本不小但必须做。搜document.getElementById和document.getElementByName,逐个换成对应封装方法。视觉上判断标准很简单:禁用之后点图标不该弹出日历,能弹就是没换干净。
5.4 现象:单机跑得好好的,一上集群缓存就各节点不一致
2.55 之前的缓存是进程内的,多节点部署时每个节点各存一份,A 节点更新了缓存,B 节点还是旧值,用户刷新几次看到的数据不一样。
原因是缓存实现没有走集群共享。新版本支持共享,前提是每个应用把 quickstart 升到 2.35 及以上,然后参照etc/common/ehcache.xml改造。实际操作不是整份覆盖,而是拿 quickstart 里那份文件的增量部分,合并到自己的配置里,保留业务自定义的 cache 定义。整份覆盖会把自定义缓存全冲掉,这是另一个坑。
改完的验证方法:两个节点分别触发一次写操作,然后在另一个节点读,能读到新值说明共享生效。只看日志里有没有「cache initialized」不算数。
5.5 现象:本地 JDK 1.5 编译通过,部署到 1.4 环境启动报 UnsupportedClassVersionError
2.59 加了JDK_VERSION参数,就是为这个场景准备的。不配这一项,Ant 用当前环境的 JDK 编译,产物版本跟着环境走。
解决是在 build.properties 里显式声明JDK_VERSION=1.4或1.5:
# 编译目标 Java 版本,与运行环境保持一致 # 部署环境是 1.4 就必须写 1.4,否则 class 文件版本偏高,运行期直接拒绝加载 JDK_VERSION = 1.4配完之后还要确认编译用的 JDK 本身支持-target 1.4,高版本 JDK 已经移除了对老 target 的支持,会报target release 1.4 conflicts with default source release。这种情况只能装一个对应版本的老 JDK 专门做编译。这类问题的排查顺序是:先看运行环境的java -version,再看 class 文件版本(javap -verbose看 major version),最后核对构建配置,别一上来就怀疑代码。
6. 改完一个 CRUD 页面后我会跑的六步检查
这套框架的反馈回路偏慢:编译不报错不代表页面能用,页面能打开不代表数据对。我自己的习惯是每改完一个业务页面,按固定顺序走一遍,六步下来大部分问题当场暴露,不用等测试提。
| 步骤 | 检查动作 | 通过标准 |
|---|---|---|
| 1 | 全局搜新页面名,看page.application和sidebar.html是否都注册了 | 两处都有,无 quickstart 残留 |
| 2 | 编译走ant all,确认 classes 目录下有新 Page 类 | 无编译告警,类文件时间戳是新的 |
| 3 | 打开列表页,看菜单项和按钮是否渲染 | 按钮样式类生效,无裸链接 |
| 4 | 走一遍新增 → 查看 → 修改 → 删除的完整链路 | 每步提示文案正确,主键能传递 |
| 5 | 修改页故意留空一个字段提交 | 用 save 时原值保留,用 update 时字段被清空(确认选对了方法) |
| 6 | 打开两个标签页同时改同一条记录 | 后提交的一方收到「已被操作过」提示 |
第 5 步是专门用来验证save/update选型的。这条我以前吃过亏:一个客户信息编辑页,表单上只放了五个字段,后台用了update,结果每次保存都把没在表单上的备注、标签、扩展属性全清空了。上线三天才发现,数据已经没法回滚,只能从备份里捞。从那以后我每写一个更新方法,都先数一遍 data 里的键和表里的列对不对得上,对不上就强制走save,性能差一点认了。
第 6 步验证的是乐观并发那套条件列。做法是开两个窗口加载同一条记录,第一个窗口改完提交,第二个窗口再提交,看是否被拦。拦不住说明更新条件里漏了状态列,或者 Page 层没处理false返回值直接忽略了。这一步用两个人配合测更真实,一个人操作容易自己骗自己。
还有一个进阶技巧值得单独提:给PageData的取参加一层兜底日志。pd.getParameter("VIP_ID")取不到时返回 null,代码里如果不判空直接拼查询条件,会变成全表扫描或者空指针。我的习惯是在每个 Page 事件的入口打一行「页面名 + 关键参数」,日志量不大,但排查「为什么这条记录查不出来」的时候能一眼看出是参数没传过来还是传过来是空。
public void queryVipcust(IRequestCycle cycle) throws Exception { PageData pd = getPageData(); String vip_id = pd.getParameter("VIP_ID"); // 入口日志:参数为空是最常见的一类问题,先定位再往下查 log.debug("queryVipcust enter, vip_id=" + vip_id); if (vip_id != null) { ExamplesBean bean = new ExamplesBean(); IData info = bean.queryVipcust(pd, vip_id); if (info == null) { // 记录不存在要用业务提示,不要抛空指针让页面白屏 common.error("您要查看的记录已不存在"); } setInfo(info); } }这段代码本身没什么技术含量,价值全在那一行日志和 null 分支上。老系统里最耗时间的不是写功能,是定位「数据为什么没出来」,而这条链路上任何一个环节断掉,表现都是同一个空白页面。把入口参数、Bean 返回、DAO 影响行数这三个点各打一行日志,排查时间能从半天压到十分钟。
这套东西放到今天看确实旧,Ant 构建、HTML 组件、GB2312 编码,每一样都和现在的技术栈隔着代。但它把一个业务系统的骨架拆得很干净:新建工程有固定套路,增删改查有固定分层,批量和导入有固定模板,版本升级有逐条 checklist。接这类工程的时候,先照着文档把这几条走通,比上来就重构靠谱得多。希望帮到你。
本文还有配套的精品资源,点击获取