前行字段、JContext 與展示列各歸其位)
JQuick-Excel 多字段 TRANSFORM讓當(dāng)前行字段、JContext 與展示列各歸其位tags: #Java #JQuickExcel #Excel導(dǎo)出 #TRANSFORM #SPI簡(jiǎn)介Excel 導(dǎo)出中的字段轉(zhuǎn)換難點(diǎn)通常不在于把一個(gè)字符串改成大寫而在于一張報(bào)表同時(shí)有當(dāng)前行字段、公共字典、日期值和派生展示列。JQuick-Excel 的TRANSFORM為這類需求提供了聲明式位置${field}讀取當(dāng)前行字段${key}也可以讀取JContext中的值函數(shù)接收完成求值后的參數(shù)并返回轉(zhuǎn)換結(jié)果。本文只依據(jù) README-CN 說(shuō)明這一邊界使用內(nèi)置toUpper、dateFormat、trans并以fullName說(shuō)明多字段派生列的組織方式。這里的“多字段”有兩層含義。第一層是一個(gè)函數(shù)可以引用同一行的多個(gè)字段例如把名和姓組合為顯示名稱。第二層是同一份TRANSFORM可同時(shí)處理文本、日期和字典編碼。它并不意味著可以在 XML 中任意編排業(yè)務(wù)流程。報(bào)表 XML 適合描述字段到單元格之間穩(wěn)定、可驗(yàn)證的轉(zhuǎn)換查庫(kù)、遠(yuǎn)程調(diào)用、權(quán)限判定和批處理策略仍應(yīng)由應(yīng)用層處理。前言真實(shí)報(bào)表往往不是把 Java 對(duì)象原樣寫進(jìn)工作簿。學(xué)生名單可能需要將姓名統(tǒng)一大寫把性別編碼翻譯為文案把入學(xué)日期按固定格式展示并額外輸出一個(gè)“顯示名稱”列。若所有這些規(guī)則都分散在 Java 循環(huán)里讀 XML 的人無(wú)法知道列值如何得到若把數(shù)據(jù)準(zhǔn)備也塞進(jìn)表達(dá)式又會(huì)使逐行導(dǎo)出的行為不可預(yù)測(cè)。我會(huì)先把參與數(shù)據(jù)分成三類。當(dāng)前行字段是每條記錄獨(dú)有的輸入如firstName、lastName、gender與enrollmentDate。JContext是一次解析過(guò)程中共享的外部數(shù)據(jù)如編碼字典。派生字段是報(bào)表需要而源對(duì)象未必原生具備的輸出如displayName。這種劃分不是額外框架規(guī)則而是組織報(bào)表的實(shí)踐讀者能清楚判斷一個(gè)值來(lái)自當(dāng)前行、來(lái)自上下文還是由函數(shù)計(jì)算得到。隨后再確定列責(zé)任。MAPPING定義源字段與表頭的映射TRANSFORM定義寫出前的字段轉(zhuǎn)換FORMAT管理最終 Excel 單元格顯示格式。README-CN 明確指出FORMAT獨(dú)立負(fù)責(zé)轉(zhuǎn)換后的 Excel 單元格顯示。因而日期規(guī)則要區(qū)分“值如何被格式化”與“單元格如何顯示”前者是dateFormat后者是FORMAT。將兩者混為一談是排查日期問(wèn)題時(shí)最常見(jiàn)的誤區(qū)。多字段規(guī)則還應(yīng)保持可追蹤性。一個(gè)派生列的 key 應(yīng)明確出現(xiàn)在MAPPING表達(dá)式中的${...}應(yīng)能在當(dāng)前行或JContext中找到來(lái)源函數(shù)名應(yīng)來(lái)自已確認(rèn)的函數(shù)集合。這樣字段改名、列重排和字典調(diào)整時(shí)修改范圍可控也能為每個(gè)字段準(zhǔn)備清晰的驗(yàn)收樣本。環(huán)境與依賴README-CN 給出的核心 Maven 坐標(biāo)是io.github.paohaijiao:jquick-excel:3.6.0。JQuick-Excel 要求 Java 8 或更高版本支持xls與xlsx。本篇基礎(chǔ)示例只依賴核心庫(kù)和 README-CN 已確認(rèn)的內(nèi)置函數(shù)不把未確認(rèn)的表達(dá)式函數(shù)當(dāng)作前提。XML 定義應(yīng)位于類路徑中并由 XML 工廠結(jié)合導(dǎo)出解析器創(chuàng)建服務(wù)代理。dependencygroupIdio.github.paohaijiao/groupIdartifactIdjquick-excel/artifactIdversion3.6.0/version/dependencyJava 側(cè)負(fù)責(zé)準(zhǔn)備行數(shù)據(jù)、輸出流以及本次導(dǎo)出共享的上下文。XML 側(cè)負(fù)責(zé)EXPORT WITH、MAPPING、TRANSFORM與FORMAT。DSL 的關(guān)鍵字、花括號(hào)、逗號(hào)、字段名和表達(dá)式分隔符都屬于配置契約修改時(shí)不能依靠“看起來(lái)相近”的寫法替換。特別是${dict}與${gender}的含義不同前者應(yīng)指向上下文字典后者應(yīng)指向當(dāng)前行的編碼字段。如果業(yè)務(wù)確實(shí)需要fullName這樣的跨報(bào)表函數(shù)則需要按 SPI 提供者方式實(shí)現(xiàn)并在運(yùn)行時(shí)類路徑中提供它。本文在 XML 中用fullName(${firstName},${lastName})展示多字段引用的形態(tài)其 provider 的具體實(shí)現(xiàn)與服務(wù)文件將在自定義 SPI 主題中單獨(dú)討論。內(nèi)置函數(shù)部分只使用toUpper、dateFormat與trans。代碼示例下面的導(dǎo)出規(guī)則有六列原始名、原始姓、派生顯示名稱、性別、年齡與入學(xué)日期。displayName的輸出位置由MAPPING明確聲明lastName使用內(nèi)置toUpper性別通過(guò)JContext中的dict使用內(nèi)置trans翻譯日期使用內(nèi)置dateFormat并保留FORMAT作為單元格顯示規(guī)則。示例不假設(shè)轉(zhuǎn)換規(guī)則之間存在未文檔化的依賴或執(zhí)行順序。excelnameexportExcelreturnClassvoid![CDATA[ EXPORT WITH SHEETStudents, HEADERtrue, MAPPING{ firstName:First Name, lastName:Last Name, displayName:Display Name, gender:Gender, age:Age, enrollmentDate:Enrollment Date }, TRANSFORM{ displayName:fullName(${firstName},${lastName}), lastName:toUpper(${lastName}), gender:trans(${dict},${gender}), enrollmentDate:dateFormat(${enrollmentDate},yyyy-MM-dd) }, FORMAT{enrollmentDate:yyyy-MM-dd} ]]/excelREADME-CN 的導(dǎo)入示例展示了JContext的構(gòu)造與put用法本文只以它說(shuō)明${dict}所代表的上下文字典語(yǔ)義不推導(dǎo) README 未展示的導(dǎo)出上下文注入 API。導(dǎo)出數(shù)據(jù)仍可按JObjectConverter和JQuickRow的已確認(rèn)路徑準(zhǔn)備。ListJQuickRowrowsJQuickRow.toRows(JObjectConverter.convert(people));try(OutputStreamoutputnewFileOutputStream(students.xlsx)){JQuickParseHandlerparsernewJQuickExcelExportXmlParseFactory(rows,output);JQuickExcelExportServiceservicenewJQuickXmlFactory(parser,jquick-excel.xml).createApi(JQuickExcelExportService.class);service.exportExcel(field,value);}驗(yàn)收時(shí)不要只打開(kāi)文件看一條正常數(shù)據(jù)。至少應(yīng)準(zhǔn)備名和姓都存在的記錄用于檢查displayName不同大小寫的姓用于檢查toUpper字典中存在的性別編碼用于檢查trans日期值用于檢查dateFormat與FORMAT以及缺失字段、未知編碼和異常日期等邊界數(shù)據(jù)。對(duì)每種樣本同時(shí)檢查列位置、單元格值和顯示形式才能區(qū)分映射錯(cuò)誤、上下文錯(cuò)誤與格式錯(cuò)誤。原理說(shuō)明README-CN 給出的轉(zhuǎn)換模型足以解釋這份規(guī)則當(dāng)前行字段或JContext值經(jīng)${...}取值后進(jìn)入TRANSFORM表達(dá)式求值后的參數(shù)交給內(nèi)置求值器或 SPI provider函數(shù)返回轉(zhuǎn)換結(jié)果最后FORMAT控制 Excel 顯示。這里最重要的事實(shí)是函數(shù)接收的是參數(shù)列表而不是 XML 文本因此函數(shù)的輸入語(yǔ)義由字段值和上下文值決定函數(shù)名和參數(shù)順序必須穩(wěn)定。當(dāng)前行字段隨記錄變化。每處理一行${firstName}、${lastName}、${gender}和${enrollmentDate}應(yīng)對(duì)應(yīng)這一行的數(shù)據(jù)。JContext中的dict則是共享數(shù)據(jù)適合字典類轉(zhuǎn)換。將公共字典重復(fù)放入每個(gè)業(yè)務(wù)對(duì)象沒(méi)有必要將當(dāng)前行數(shù)據(jù)偽裝成共享上下文也會(huì)讓來(lái)源含混。把兩者分開(kāi)后出現(xiàn)空值時(shí)可以先判斷源字段是否缺失再判斷上下文是否準(zhǔn)備完成。派生字段不是特殊類型。displayName的意義只是一個(gè)被映射到輸出列的字段其值由fullName返回。關(guān)鍵不在于源對(duì)象中是否預(yù)先存在該屬性而在于報(bào)表規(guī)則是否為它聲明了明確列位和明確輸入。對(duì)于只想標(biāo)準(zhǔn)化已有字段的情況例如將lastName轉(zhuǎn)大寫直接以原字段作為TRANSFORMkey 更直觀。對(duì)于要新增展示列的情況則讓派生字段同時(shí)出現(xiàn)在MAPPING與TRANSFORM中。日期的兩個(gè)層次需要單獨(dú)驗(yàn)證。dateFormat(${enrollmentDate},yyyy-MM-dd)是內(nèi)置函數(shù)調(diào)用FORMAT{enrollmentDate:yyyy-MM-dd}是單元格顯示控制。README-CN 已明確二者職責(zé)獨(dú)立。出現(xiàn)日期異常時(shí)先確認(rèn)函數(shù)輸入和返回是否符合預(yù)期再確認(rèn)工作簿中單元格顯示格式避免僅修改一處而掩蓋另一處的問(wèn)題。多字段轉(zhuǎn)換的可維護(hù)性來(lái)自小而明確的規(guī)則。toUpper只處理文本大寫dateFormat只處理日期格式trans只做上下文字典映射fullName只組合兩個(gè)輸入。不要在逐行函數(shù)中加入數(shù)據(jù)庫(kù)查詢、網(wǎng)絡(luò)請(qǐng)求或依賴共享可變狀態(tài)的邏輯。此類行為既不屬于 README-CN 所描述的轉(zhuǎn)換契約也會(huì)使性能、異常處理和結(jié)果一致性難以控制。導(dǎo)入和導(dǎo)出都可以使用TRANSFORM但業(yè)務(wù)方向并不相同。導(dǎo)出通常把編碼轉(zhuǎn)換成給人看的文案導(dǎo)入可能需要反向處理。不能因?yàn)閮蓚?cè)語(yǔ)法相同就不加驗(yàn)證地復(fù)制同一份字典規(guī)則。每個(gè)方向都應(yīng)有獨(dú)立的輸入樣本和預(yù)期輸出尤其是日期、編碼和空值數(shù)據(jù)。注意事項(xiàng)第一派生字段要在MAPPING中有明確輸出列。只寫TRANSFORM{displayName:...}而沒(méi)有對(duì)應(yīng)列會(huì)使報(bào)表設(shè)計(jì)失去清晰的落點(diǎn)。字段名調(diào)整時(shí)應(yīng)同步檢查 MAPPING key、${field}引用、TRANSFORM key 與 FORMAT key。第二創(chuàng)建解析器前準(zhǔn)備JContext。${dict}的鍵名必須與context.put(dict, gender)相同。字典應(yīng)由應(yīng)用層準(zhǔn)備為本次導(dǎo)出穩(wěn)定可用的數(shù)據(jù)未知編碼的業(yè)務(wù)展示結(jié)果應(yīng)通過(guò)樣本顯式驗(yàn)證而不是假定它會(huì)自動(dòng)得到某個(gè)文案。第三只調(diào)用確認(rèn)存在的函數(shù)。核心內(nèi)置函數(shù)以 README-CN 明示的toUpper、dateFormat、trans為準(zhǔn)。fullName是自定義 SPI 示例只有 provider 和服務(wù)資源實(shí)際在類路徑中時(shí)才可調(diào)用。不要根據(jù)其他 Java 工具庫(kù)的習(xí)慣猜測(cè)函數(shù)名、嵌套能力或自動(dòng)類型轉(zhuǎn)換。第四不要把FORMAT當(dāng)成業(yè)務(wù)轉(zhuǎn)換也不要把業(yè)務(wù)轉(zhuǎn)換當(dāng)成 Excel 公式。FORMAT管顯示TRANSFORM管導(dǎo)出前的值轉(zhuǎn)換FORMULAS是另一項(xiàng)工作簿公式功能。名稱相似不代表執(zhí)行時(shí)機(jī)和結(jié)果類型相同。第五轉(zhuǎn)換路徑按行運(yùn)行。復(fù)雜邏輯應(yīng)在導(dǎo)出前完成函數(shù)保持無(wú)狀態(tài)、確定且低開(kāi)銷。對(duì)于大數(shù)據(jù)導(dǎo)出數(shù)據(jù)準(zhǔn)備、字典構(gòu)建和批次控制更應(yīng)該放在應(yīng)用層避免每行重復(fù)做昂貴工作。第六保留可讀性。一個(gè)字段需要多個(gè)難以解釋的條件時(shí)與其把 XML 堆成不可審查的表達(dá)式不如先明確業(yè)務(wù)口徑并將跨報(bào)表復(fù)用的穩(wěn)定邏輯實(shí)現(xiàn)為命名清晰的 SPI provider。XML 應(yīng)讓維護(hù)者看懂列從哪里來(lái)、經(jīng)過(guò)什么轉(zhuǎn)換、最終怎樣展示。規(guī)則拆分與驗(yàn)證多字段模板的審查重點(diǎn)應(yīng)是每個(gè)輸出字段的依賴范圍。以displayName為例它依賴當(dāng)前行的firstName和lastName性別列還依賴共享的${dict}日期列則依賴當(dāng)前行日期值和格式參數(shù)。將依賴寫成這樣的清單不會(huì)改變 DSL 行為卻可以避免維護(hù)者把上下文值、當(dāng)前行值與表頭名稱混為同一類輸入。派生字段的驗(yàn)收還要確認(rèn)它不會(huì)取代原始字段。示例中displayName是單獨(dú)映射的列firstName、lastName仍各有輸出位置而lastName的toUpper是對(duì)已有輸出字段的值轉(zhuǎn)換。這兩種寫法分別表達(dá)“新增展示字段”和“修改指定字段的寫出值”。若需求只要求其中一種應(yīng)避免同時(shí)配置兩種以免工作簿出現(xiàn)重復(fù)卻含義不清的列。同一份TRANSFORM中的規(guī)則不應(yīng)建立在未確認(rèn)的配置項(xiàng)先后順序上。每一項(xiàng)應(yīng)只依賴當(dāng)前行可獲得的字段、JContext中已準(zhǔn)備的值和函數(shù)顯式參數(shù)。對(duì)于需要把一個(gè)轉(zhuǎn)換結(jié)果再作為另一個(gè)規(guī)則輸入的需求不能從本文所依據(jù)的事實(shí)推導(dǎo)其執(zhí)行順序應(yīng)先在應(yīng)用層準(zhǔn)備明確字段或以項(xiàng)目實(shí)際版本的行為驗(yàn)證后再采用。這樣不會(huì)將隱含順序變成報(bào)表契約。日期字段應(yīng)從值和顯示兩個(gè)角度驗(yàn)收。dateFormat是內(nèi)置轉(zhuǎn)換函數(shù)FORMAT是單元格顯示配置樣本檢查應(yīng)記錄轉(zhuǎn)換后的字段結(jié)果以及在 Excel 中看到的顯示文本。若業(yè)務(wù)還要對(duì)導(dǎo)出的日期做后續(xù)計(jì)算則需要由項(xiàng)目根據(jù)實(shí)際類型和目標(biāo)模板驗(yàn)證可計(jì)算性不能僅因顯示為yyyy-MM-dd就推斷其數(shù)據(jù)語(yǔ)義。字典轉(zhuǎn)換也應(yīng)單獨(dú)覆蓋方向和范圍。trans(${dict},${gender})表達(dá)式的字典來(lái)自JContext當(dāng)前編碼來(lái)自處理中的行當(dāng)同一報(bào)表導(dǎo)出多批數(shù)據(jù)時(shí)應(yīng)用層應(yīng)確保為該批次準(zhǔn)備的字典符合業(yè)務(wù)范圍。TRANSFORM只針對(duì)當(dāng)前行求值不負(fù)責(zé)識(shí)別某個(gè)編碼是否在其他行出現(xiàn)過(guò)也不負(fù)責(zé)決定字典的加載來(lái)源。對(duì)于 SPI 形式的fullName模板的可移植條件是 provider 實(shí)際可用。它的存在不能僅由 XML 中的函數(shù)名證明。內(nèi)置函數(shù)與自定義函數(shù)在規(guī)則中可以并列出現(xiàn)但驗(yàn)收應(yīng)將它們分開(kāi)內(nèi)置toUpper、dateFormat、trans按核心已確認(rèn)能力測(cè)試fullName則按項(xiàng)目提供的 SPI provider 與運(yùn)行時(shí)類路徑測(cè)試。這樣函數(shù)缺失時(shí)能準(zhǔn)確定位為擴(kuò)展部署問(wèn)題而不會(huì)誤判字段映射。總結(jié)多字段TRANSFORM的關(guān)鍵是輸入來(lái)源清楚當(dāng)前行字段提供記錄級(jí)數(shù)據(jù)JContext承擔(dān)共享字典函數(shù)只生成目標(biāo)列的值。姓名組合、狀態(tài)翻譯和文本標(biāo)準(zhǔn)化可以寫在 DSL 中但原始字段仍應(yīng)保留清晰語(yǔ)義避免展示值反過(guò)來(lái)參與后續(xù)計(jì)算。驗(yàn)證時(shí)同時(shí)覆蓋正常記錄、缺失字段和字典未命中的情況并核對(duì)轉(zhuǎn)換后的值是否仍符合列的使用方式。一次性展示規(guī)則留在內(nèi)置能力范圍內(nèi)確實(shí)需要跨報(bào)表復(fù)用時(shí)再將邏輯放入 SPI provider避免 XML 和業(yè)務(wù)準(zhǔn)備層重復(fù)承擔(dān)同一段轉(zhuǎn)換。