最近項目需要使用上傳EDI封包,然後解析EDI封包格式并持久化到資料庫。然後在網上找了一下,有個非常強大的smooks元件可以實作這樣的功能。而且不僅僅隻實作從EDI -> Java的轉化,還可以多種格式互相轉化,唯一要做的就是配置兩個非常重要的XML配置檔案。
下面是關于smooks的比較詳細的中文介紹:
Java的XML轉換架構 Smooks
下面是smooks的下載下傳位址,由于smooks的官網是屬于河蟹的範圍,然後我一直都懶得去翻牆通路了,詳細的API在這裡也能下載下傳到。
smooks下載下傳
現在的最新版本為 1.5.1
下載下傳完成後解壓,可以直接進入examples 檔案夾,裡面有smooks提供的所有功能的示例程式,部分示例程式使用了JUnit測試。
是以測試的時候可以直接把lib下的所有檔案都導入進去。

上面就是所有的示例,可以看見smooks提供很多格式檔案的轉化功能,個人覺得比較常用的應該就是EDI XML CSV Java 之間的互相轉換了吧。然後這次我主要關注的就是edi-to-java功能!
然後在這個示例程式裡面,有标準的EDI封包格式,據帶我的師傅說,這個EDI格式有很多種标準,由于這個smooks架構是國外的,是以他裡面的EDI封包格式可能是國外的标準,然後我們國家有自己的EDI封包格式标準,有的公司有的行業或者又有他們自己規定的封包格式,是以這次我主要讨論比較複雜的自定義的封包格式怎麼解析成JavaBean。
首先你要根據你自己的封包格式定義好各個JavaBean類。比如我的就複雜一點。我的是一個總類,總類下有公共資訊,然後還有訂單清單,每個訂單下又有訂單資訊和貨物資訊和箱型資訊清單。
檔案格式如下
BillConfirm
|- Ship
|- List<ShipBillConfirm>
|- (n * Fields)
|- List<ShipBillConfirmCargo>
|- ShipBillConfirmCargo
|- (n * Fields)
|- List<ShipBillConfirmCntr>
|- ShipBillConfirmCntr
|- (n * Fields)
這個就比标準結構複雜一點了,其實那個BillConfirm是不用放到資料庫的,關鍵要關心的就是多個訂單ShipBillConfirm,但是由于一個EDI裡面可以有多個訂單資訊,是以在smooks中就要使用List,是以那個BillConfirm就隻是一個容器而已。Ship是所有訂單的公共資訊。
下面給出測試的EDI封包格式。
00:BOOKOF:BOOKING ORDER FROM:9:SWS::200912281627'
10:HENG YU:1001E::20100105:SHANGHAI'
12:SJJ:JJNH4848240ATEST1:CY/CY:P::KOBE::KOBE::P&H CO.,LTD::NOPAL INT?'L C.S.CO.,LTD TEL?:045-253-8213 FAX?:045-253-8214::SAME AS CONSIGNEE::'
41:1:O:1:1.00::1.000:N/M:SPORTWEAR'
41:2:O:2:3.00::3.000:N/M:SPORTWEAR22'
12:SJJ:JJNH4848240ATEST2:CY/CY:P::KOBE::KOBE::P&H CO.,LTD::NOPAL INT?'L C.S.CO.,LTD TEL?:045-253-8213 FAX?:045-253-8214::SAME AS CONSIGNEE::'
41:1:O:1:11.00::1.000:N/M:SPORTWEAR333'
99:9
這是一段比較簡單的EDI封包格式了,其實實際中我的項目用到的比這個還要複雜一點,但是現在就隻是測試和學習,我就改了一下,改得比較簡單了。
下面就根據EDI封包格式和JavaBean來寫配置檔案。
然後smooks示例中解析的主要代碼是在Main.Java中,去看代碼就發現,關鍵的就是三個檔案,一個是封包檔案,這個可以我們自己修改擷取的方式來解析,然後另外就是兩個XML配置檔案,一個是smooks-config.xml,一個是edi-to-java-order-mapping.xml。
然後就主要就是修改這兩個檔案。
其中smooks-config.xml是主配置檔案,是用來把解析的東西來生成JavaBean的。
edi-to-java-order-mapping.xml是來比對封包格式的配置檔案。
其中smooks-config.xml的配置比較簡單,和Hibernate差不多,用過的都比較容易上手。下面是我根據我的測試要求配置的:
<?xml version="1.0"?>
<smooks-resource-list xmlns="http://www.milyn.org/xsd/smooks-1.1.xsd"
xmlns:edi="http://www.milyn.org/xsd/smooks/edi-1.1.xsd" xmlns:jb="http://www.milyn.org/xsd/smooks/javabean-1.2.xsd">
<!--
Configure the EDI Reader to parse the message stream into a stream of
SAX events.
-->
<edi:reader mappingModel="/shhh/edi-confirm-mapping.xml" />
<!--
這裡就是主Bean,最後生成傳回的就是這個對象;
其中使用了<jb:wiring>的标簽,這個表示的是類的引用,
property就是類中屬性的name, beanIdRef 就是對這個檔案中的bean的引用。
然後createOnElement這個不怎麼好解釋,這個是和那個封包配置檔案綁定的,
也可以了解為在哪個類中生成下面的資料。
-->
<jb:bean beanId="bill" class="shhh.entity.BillConfirm"
createOnElement="BillConfirm">
<jb:wiring property="ship" beanIdRef="ship" />
<jb:wiring property="sbc" beanIdRef="sbcList" />
</jb:bean>
<!--
這個是在BillConfirm中的Ship屬性,由于是單個的,
是以createOnElement中需要引用${bill};
其實這個到底代表什麼意思我還沒有去深入了解,
我的暫時的了解是表示這個ship是生成在bill這個beanId下的,
但是他引用的封包配置檔案中的segment的XMLtag是"Ship";
另外的decoder就是表示這個屬性用的什麼資料類型;
data 表示的是在封包配置檔案中的引用,封包配置檔案中對應的是XMLtag;
<jb:decodeParam> 一看就知道了,這個是日期格式的轉換配置。
-->
<jb:bean beanId="ship" class="shhh.entity.Ship"
createOnElement="${bill}/Ship">
<jb:value property="voyageNo" decoder="Integer" data="#/voyage-no" />
<jb:value property="lineNam" decoder="String" data="#/line-nam" />
<jb:value property="lineCod" decoder="String" data="#/line-cod" />
<jb:value property="leavPortTim" decoder="Date" data="#/leave-date">
<jb:decodeParam name="format">yyyyMMdd</jb:decodeParam>
</jb:value>
<jb:value property="EDestPortNam" decoder="String" data="#/edestportnam" />
</jb:bean>
<!--
這個就是關鍵的訂單清單配置了,由于是有多個的清單的形式,是以要先配置清單,
BillComfirm中引用的也是這個清單,然後這個清單再引用ShipBillComfirm這個bean
-->
<jb:bean beanId="sbcList" class="java.util.ArrayList"
createOnElement="BillConfirm">
<jb:wiring beanIdRef="shipbillconfirm" />
</jb:bean>
<!--
這個就是關鍵的訂單配置了,其中這個裡面又配置了兩個清單
-->
<jb:bean beanId="shipbillconfirm" class="shhh.entity.ShipBillConfirm"
createOnElement="ShipBillConfirm">
<jb:value property="carrierCod" decoder="String" data="#/carriercod" />
<jb:value property="billNbr" decoder="String" data="#/billnbr" />
<jb:value property="drTypeId" decoder="String" data="#/drtypeid" />
<jb:value property="payWayId" decoder="String" data="#/paywayid" />
<jb:value property="dischrgPortNam" decoder="String"
data="#/dischrgportnam" />
<jb:value property="destPortNam" decoder="String" data="#/destportnam" />
<jb:value property="EShipperNam" decoder="String" data="#/eshippernam" />
<jb:value property="consigneeNam" decoder="String" data="#/consigneenam" />
<jb:value property="notifyNam" decoder="String" data="#/notifynam" />
<jb:value property="notifyNam2" decoder="String" data="#/notifynam2" />
<jb:wiring property="cargo" beanIdRef="cargoList" />
<jb:wiring property="cntr" beanIdRef="cntrList" />
</jb:bean>
<!--
貨物清單資訊
-->
<jb:bean beanId="cargoList" class="java.util.ArrayList"
createOnElement="ShipBillConfirm">
<jb:wiring beanIdRef="shipbillconfirmcargo" />
</jb:bean>
<!--
箱型清單資訊
-->
<jb:bean beanId="cntrList" class="java.util.ArrayList"
createOnElement="ShipBillConfirm">
<jb:wiring beanIdRef="shipbillconfirmcntr" />
</jb:bean>
<!--
單個貨物資訊的配置bean
-->
<jb:bean beanId="shipbillconfirmcargo" class="shhh.entity.ShipBillConfirmCargo"
createOnElement="ShipBillConfirmCargo">
<jb:value property="seqNo" decoder="Short" data="#/seqno" />
<jb:value property="cargoCod" decoder="String" data="#/cargocod" />
<jb:value property="pieceNum" decoder="Long" data="#/piecenum" />
<jb:value property="grossWeight" decoder="Double" data="#/grossweight" />
<jb:value property="netWeight" decoder="Double" data="#/netweight" />
<jb:value property="volNum" decoder="Double" data="#/volnum" />
<jb:value property="marksStr" decoder="String" data="#/marksstr" />
<jb:value property="ECargoNam" decoder="String" data="#/ecargonam" />
</jb:bean>
<!--
單個箱型資訊的配置bean
-->
<jb:bean beanId="shipbillconfirmcntr" class="shhh.entity.ShipBillConfirmCntr"
createOnElement="ShipBillConfirmCntr">
<jb:value property="cntrNo" decoder="String" data="#/cntrno" />
<jb:value property="cntrSizeCod" decoder="String" data="#/cntrsizecod" />
<jb:value property="cntrTypeCod" decoder="String" data="#/cntrtypecod" />
</jb:bean>
</smooks-resource-list>
然後下面就是關鍵的解析EDI封包格式的mapping檔案了:
<?xml version="1.0" encoding="UTF-8"?>
<medi:edimap xmlns:medi="http://www.milyn.org/schema/edi-message-mapping-1.3.xsd">
<medi:description name="Ship Bill Confirm" version="1.0" />
<medi:delimiters segment="'" field=":" component="^" sub-component="~" escape="?" />
<medi:segments xmltag="BillConfirm">
<medi:segment segcode="00" xmltag="Total">
<medi:field xmltag="edi-name" />
<medi:field xmltag="edi-des" />
<medi:field xmltag="edi-func" />
<medi:field xmltag="edi-user" />
<medi:field xmltag="edi-empty1" />
<medi:field xmltag="edi-create" />
</medi:segment>
<medi:segment segcode="10" xmltag="Ship">
<medi:field xmltag="voyage-no" />
<medi:field xmltag="line-nam" />
<medi:field xmltag="line-cod" />
<medi:field xmltag="leave-date" />
<medi:field xmltag="edestportnam" />
</medi:segment>
<medi:segmentGroup maxOccurs="-1" minOccurs="0">
<medi:segment segcode="12" xmltag="ShipBillConfirm" maxOccurs="-1" minOccurs="1">
<medi:field xmltag="carriercod" />
<medi:field xmltag="billnbr" />
<medi:field xmltag="drtypeid" />
<medi:field xmltag="paywayid" />
<medi:field xmltag="dischrgportcod" />
<medi:field xmltag="dischrgportnam" />
<medi:field xmltag="destportcod" />
<medi:field xmltag="destportnam" />
<medi:field xmltag="shipperid" />
<medi:field xmltag="eshippernam" />
<medi:field xmltag="consigneeid" />
<medi:field xmltag="consigneenam" />
<medi:field xmltag="notifyid" />
<medi:field xmltag="notifynam" />
<medi:field xmltag="notifyid2" />
<medi:field xmltag="notifynam2" />
</medi:segment>
<medi:segment segcode="41" xmltag="ShipBillConfirmCargo" maxOccurs="-1" minOccurs="0">
<medi:field xmltag="seqno" />
<medi:field xmltag="cargocod" />
<medi:field xmltag="piecenum" />
<medi:field xmltag="grossweight" />
<medi:field xmltag="netweight" />
<medi:field xmltag="volnum" />
<medi:field xmltag="marksstr" />
<medi:field xmltag="ecargonam" />
</medi:segment>
<medi:segment segcode="51" xmltag="ShipBillConfirmCntr" maxOccurs="-1" minOccurs="0">
<medi:field xmltag="cntrno" />
<medi:field xmltag="cntrsizecod" />
<medi:field xmltag="cntrtypecod" />
</medi:segment>
</medi:segmentGroup>
<medi:segment segcode="99" xmltag="End" minOccurs="0">
<medi:field xmltag="endNumLine" />
</medi:segment>
</medi:segments>
</medi:edimap>
其中有幾個關鍵的鍵我解釋一下:
<medi:description> 這個就是一個名字定義,沒有太大影響;
<medi:delimiters segment="'" field=":" component="^" sub-component="~" escape="?" />
這個裡面,segment 表示的是每個資料(也可以了解為每個Bean資訊)的分割符,
field 表示的是每個字段的分隔符,也就是按順序分割資料,然後儲存到Bean中的屬性中(按照你寫配置解析EDI的順序儲存)
component 和 sub-component 暫時我還沒用到,看示例裡面好想是用來生成XML時的子節點。
然後還有一個關鍵的escape 這個是轉義字元
然後我的Myeclipse不知怎麼的用不了自動提示,不知道有哪些節點哪些屬性可以用,害我隻好把mapping的結構定義檔案給下過來了 ╮(╯▽╰)╭,然後就慢慢看吧 = =。
其中<medi:segments>隻能有一個,表示就是解析這個EDI檔案的東西。
然後<medi:segment>就是每個解析的Bean的配置,用的就是那個每個資料的分隔符給分割的。
segcode 就是表示字段的開始字元,表示這個segmen的開始标志就是這個segcode。
xmltag 前面說過了就是和引用有關的,不能有重複的。
<medi:field xmltag="edi-name" /> 這個就是每個field字段了,根據你寫這個的順序來解析EDI并比對到引用的xmltag中去。
然後關鍵的難點就是有個<medi:segmentGroup maxOccurs="-1" minOccurs="0"> 這個東西,這個在示例裡面是沒有的,在API裡面我也沒看到,愣是在結構檔案裡面看到的!
這個就是相當與一個循環結構,表示下面配置的segment在EDI封包可以循環比對。其實就是我前面提到的,一個EDI可以有多個訂單資料,每個訂單資料下又可以有多個貨物資訊和多個箱型資訊。
是以下面就是會循環的資料配置。
最後有兩個關鍵的屬性:
maxOccurs:這個表示的是有多少個資料比對,-1表示可能有多個資料比對,意思就是可以有循環的
minOccurs:這個表示的是這個segment的出現的最少次數,0表示這個segment可能是不會出現的。(如果有可能不會出現的資料,這個必須設定為0,不然在解析的時候是會報錯的,因為這個好像預設是1)
最後還有一點,可能遇到EDI封包裡面有一個格式字段,但是我們不需要,但是在mapping配置裡面還是要配置的,你可以不引用,但是不能無視掉,是以我最後還配置了一個99 的segment,但是沒有引用。
等于就是smooks執行的是嚴格比對查詢,不是什麼你隻需要哪一段資料。是以關鍵的就是要根據固定的EDI格式來寫好配置檔案。
其實寫好後,就算遇到EDI格式更改,你也隻需要修改一下兩個配置檔案就可以繼續用了,擴充性還是不錯的。
然後其實這個EDI解析還可以直接生成XML檔案的,在示例裡面有,簡單的兩句代碼就可以了,由于我暫時還沒用到這個功能是以也就沒有介紹了,感興趣的可以去下載下傳示例源碼看看。
大概就是這些,如果還有什麼東西再分享吧。然後這個方面應該有相關的大神,但是不知道為什麼就是沒有找到相關的介紹,這個畢竟在商業交流中還是用得比較廣泛的。如果有大神路過,希望不吝指教。