1.概述
我們要將外部系統給的XML文件進行解析,並存入到數據庫。
但是我們並沒有DTD或者Schema,只有一個WORD格式的說明文檔;更離譜的是,XML結點樹的結構(即XML結點與XML結點之間的關系)與業務Bean樹的結構(即業務Bean與業務Bean的關系)並不完全一致,比如說,從業務角度講,一只豬有只豬頭,而在XML裡,卻寫成了 pig --content --pighead 的三級關系,無端端多了一個content結點! 沒有DTD/Schema,結構又不規范,我們就沒法用自動化的第三方JAVA轉換API進行解析,而只能手動地、一個一個地解析。但在手動解析的過程中,我們仍然發現各個結點的解析和入庫中有很多東西是共同的,或者有共同的規律,這些東西可以抽出來作為一個准框架,然後再將結點中不同的部分開放出來,允許具體的結點做具體的實現,並最終形成一個半自動的解析/入庫框架。
為什麼說它是半自動的?它有哪些限制?
自動:不必為每個結點編寫XML 解析代碼和入庫代碼
“半”:需手動地編寫每個JAVABEAN,並手動地為每個BEAN建表
限制:
a.所有業務字段的類型只能設為STRING/VARCHAR,並且非業務字段的類型在BEAN中不能為STRING
b.BEAN名與表名必須相同,或者可以進行一對一映射
c.BEAN的成員變量名必須與XML結點的屬性名/元素名相同,或者可以進行一對一映射
這三種限制都是利用JAVA反射機制進行自動操作的前提。
2.基本思想
所謂的XML解析,就是將XML結點轉換成JAVABEAN實例,XML結點的ATTRIBUTE值和ELEMENT值就是JAVABEAN實例的成員變量值; 所謂的持久化,就是將JAVABEAN實例變成數據庫對應表中的一條記錄,JAVABEAN實例的成員變量值就是記錄中某個字段的值,或者其他表中某個參考了該記錄的另一條記錄。
而在XML中,JAVABEAN體系中,數據據表關系結構中,結點和結點之間的關系都是樹形的關系。整體的解析和入庫,就是在遍歷樹時執行轉換動作。而我們知道,樹的遍歷是可以用遞歸算法實現的,而遞歸,就不用說了吧,它是實現程序“自動化”的主要途徑之一。
以下是對各“樹”的具體分析:
假設兩個業務實體A和B之間存在聚合關系(父子關系)。那麼具體可分三種情況:
a.B是一個原子字段(即不可再分),並且是A的一個屬性。
XML中,B是A的XML ATTRIBUTE或者A的原子ELEMENT
BEAN中,B是A的成員變量,並且B是一個JAVA內置的數據類型
數據庫中,B是A表的一個列
b.B是一個復合字段,並且是A的一個屬性,而且和A是1:1關系
XML中,B是A的ELEMENT,並且B有自己的ELEMENT或者ATTRIBUTE
BEAN中,B是A的成員變量,並且程序中有個B類
數據庫中,B表是A表的子表(即B外鍵參考了A表)
c.B是一個復合字段,並且是A的一個屬性,而且和A是N:1關系
XML中,B是A的ELEMENT,並且B有自己的ELEMENT或者ATTRIBUTE
BEAN中,B組成一個類集(List,Set)共同作為A的成員變量,並且程序中有個B類
數據庫中,B表是A表的子表(即B外鍵參考了A表)
了解了這三種情況,接下來就好辦了。程序每抓到一個結點,都要遞歸地進行以下處理:先處理它的原子屬性(情形a),接著處理它的單個子結點(情形b),最後處理它的類集子結點( 情形c)。
3.代碼實現的重點
兩個重點:
a.如何讓業務實體在三棵樹內一一對應好?
b.如何發現樹形關系,比如A的屬性有哪些,A的子結點有哪些?
問題a很簡單,就是讓三棵樹裡相同的業務實體取相同的名字。
a.解析XML時發現 結點X 的 屬性Y 等於 值Z,則執行PropertyUtils.setProperty(結點X , 屬性Y , 值Z)即可。在這裡X,Y,Z是變量,程序不用關心具體的結點和屬性是哪些個。需要注意的是,如果屬性Y是原子字段,則要求屬性Y必須為String類型,否則程序不知道將值Z轉換成哪種類型(注:關於PropertyUtils, 請見apache commons Beanutils )
b.入庫時發現x.getY()=z。如果屬性y是原子字段,則執行SQL insert into X(...,y,...) values (...,z,...),這裡要求y字段必須為varchar/char類型, 以免發生類型轉換錯誤.
關於問題b
XML樹:JDOM, dom4j等都可以直接找到父子關系
BEAN體系:
I.原子屬性。我們限定一個BEAN中所有有業務意義的原子字段的類型都STRING,所有String類型的字段都是業務字段
II.單個子結點。我們讓所有有業務意義的非原子字段都實現一個共同的接口BusiNode,這樣一個BEAN中所有BusiNode成員都是這個BEAN的子結點
III.類集子結點。我們也可以限定所有且只有類集子結點可以使用List或Set類型,這樣可以利用過濾出所有類集子結點。然而,在JAVA1.4及以前的版本裡,程序並不知道過濾出的類集子結點是哪個Class的實例(因為沒有泛型),也就沒辦法實例化一個類集子結點(見後文),因此只能手動注冊類集子結點的屬性名和Class。JAVA1.5以上的版本我沒用過,不知道可不可以解決這個問題。
數據庫表關系: 這就不用多說了,就是通過外鍵參考。因此每類結點對應的表中,都必須有個外鍵,以參考它的父結點;還必須有個主鍵,以供它的子結點參考。各表的外鍵名必須相同並為一常數,否則程序生成INSERT SQL時才可以不用理會具體表的具體的外鍵名。
程序在解析時,遍歷的是BEAN樹;在持久化時也是。比起XML樹,BEAN樹代表真正的業務結構;比起數據庫表關系樹,BEAN樹才能由父至子地進行先序遍歷
4.其他問題
a.要讓程序知道,原子屬性中哪些是XML結點的屬性,哪些是XML結點的原子ELEMENT。代碼中這是兩個抽象方法,必須讓具體的結點類實現
b.回顧本文概述部分提到的“pig --content --pighead 的三級關系,無端端多了一個content結點”,因此我們要讓程序知道,pighead,pigfoot等結點的子結點,究竟是pig,還是pig下的content。處理不規范XML時要注意這個問題。這也是一個抽象方法,必須讓具體的結點類實現
c.與上一條類似但更變態的,是類集結點的不規范問題。假設一個pig有多個pighead,那結構可能為 pig--pighead,pighead,...,也可能為pig--pigheads--content,content.... 必須讓程序知道某個具體結點用的是哪種模式
5.代碼
核心:多態 + 遞歸
a.接口BusiNode
import java.util.*;
import org.dom4j.Element;
/**
* 每個結點都要實現的接口
* 它提供了一些方法以方便實現遞歸的XML解析和持久化
*
*/
public interface BusiNode {
/**
* 所有類型為不可分類型的屬性
* @return 屬性名的集合
*/
public List getAtomicPropNames();
/**
* 一些成員變量。這些成員變量是XML結點的屬性
* @return
*/
public List getXmlAttributes();
/**
* 一些成員變量。這些成員變量是XML結點的子元素,並且類型為不可分的
* @return
*/
public List getXmlAtomicElements();
/**
* 所有類型為類集的屬性,並且這些類集中每個元素的類型都是BusiNode
* @return key = 屬性名, value = 屬性類的Class對象。
* 如果為空不返回NULL,而是空的MAP
*/
public Map getCollectionPropsMap();
/**
* 所有類型為BusiNode的屬性
* @return 屬性名的集合
*/
public List getBusiNodePropNames();
/**
* 從XML中解析出來。
* @param element
* @return
*/
public void parseFromXML(Element element);
}
b.默認的實現
import java.lang.reflect.Field;
import java.util.*;
import org.apache.commons.beanutils.PropertyUtils;
import org.dom4j.Attribute;
import org.dom4j.Element;
/**
* 默認的BUSI NODE。 繼承此類的BUSI NODE 需滿足 所有不可分屬性集=String類型的屬性集
* MyUtils類的代碼欠奉
*
*/
public abstract class DefaultBusiNode implements BusiNode {
public List getAtomicPropNames() {
return MyUtils.getFieldNamesOfClass(this.getClass(), String.class);
}
public List getBusiNodePropNames() {
return MyUtils.getFieldNamesOfClass(this.getClass(), BusiNode.class);
}
/*
* 所有子元素的父元素。有時是本結點,有時是本結點下的元素。變態
*/
public abstract Element getXmlElementParent(Element rootElement);
/*
* 類集子結點根元素的Iterator 。 假設一個pig有多個pighead,
* 那結構可能為 pig--pighead,pighead,...,
* 也可能為pig--pigheads--content,content....
* 必須讓程序知道某個具體結點用的是哪種模式
*
* 如果為空則返回一個空類集的Iterator ,不要返回NULL
*/
public abstract Iterator getCollectionElementIterator(
Element xmlElementParent, String attName);
/**
* 解析XML屬性
*
* @param rootElement
*/
protected void parseAttributesFromXml(Element rootElement) {
List xmlAttributes = this.getXmlAttributes();
for (int i = 0; i < this.getXmlAttributes().size(); i++) {
String attName = (String) xmlAttributes.get(i);
Attribute att = rootElement.attribute(attName);
if (att != null) {
try {
PropertyUtils.setProperty(this, attName, att.getValue());
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
}
/**
* 解析不可分的Element
*
* @param rootElement
*/
protected void parseAtomicElementFromXml(Element rootElement) {
Element xmlElementParent = getXmlElementParent(rootElement);
if (xmlElementParent == null) {
return;
}
List xmlElements = this.getXmlAtomicElements();
for (int i = 0; i < xmlElements.size(); i++) {
String attName = (String) xmlElements.get(i);
Element elmt = xmlElementParent.element(attName);
if (elmt != null) {
try {
PropertyUtils.setProperty(this, attName, elmt.getText());
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
}
/**
* 解析BusiNode屬性
*
* @param rootElement
*/
protected void parseBusiNodeElementFromXml(Element rootElement) {
Element xmlElementParent = getXmlElementParent(rootElement);
if (xmlElementParent == null) {
return;
}
// 再解析BusiNode屬性
List busiNodePropNames = this.getBusiNodePropNames();
for (int i = 0; i < busiNodePropNames.size(); i++) {
try {
String attName = (String) busiNodePropNames.get(i);
Element elmt = xmlElementParent.element(attName);
if (elmt != null) {
Field field = this.getClass().getDeclaredField(attName);
BusiNode att = (BusiNode) field.getType().newInstance();
att.parseFromXML(elmt);
PropertyUtils.setProperty(this, attName, att);
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
/**
* 解析類集屬性
*
* @param rootElement
*/
protected void parseCollectionPropsFromXml(Element rootElement) {
// 先解析XML屬性
Element xmlElementParent = getXmlElementParent(rootElement);
if (xmlElementParent == null) {
return;
}
// 最後解析類集屬性
Map collectionPropsMap = this.getCollectionPropsMap();
for (Iterator it = collectionPropsMap.keySet().iterator(); it.hasNext();) {
try {
String attName = (String) it.next();
Collection coll = (Collection) PropertyUtils.getProperty(this,
attName);
Class attType = (Class) collectionPropsMap.get(attName);
Iterator collElementsIt = this.getCollectionElementIterator(
xmlElementParent, attName);
// xmlElementParent.elementIterator(attName);
while (collElementsIt.hasNext()) {
Element collElmt = (Element) collElementsIt.next();
BusiNode sinlgeAtt = (BusiNode) attType.newInstance();
sinlgeAtt.parseFromXML(collElmt);
coll.add(sinlgeAtt);
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
/**
* 從XML中解析出結點。此方法可能拋出RumtimeException
*/
public void parseFromXML(Element rootElement) {
this.parseAttributesFromXml(rootElement);
this.parseAtomicElementFromXml(rootElement);
this.parseBusiNodeElementFromXml(rootElement);
this.parseCollectionPropsFromXml(rootElement);
}
}
/**
* 入庫
* JdbcUtil,MyUtils的代碼欠奉
*
*/
public class BusiNodeDAO {
private Long saveBusiNode(BusiNode node, Long parentNodeId) {
// 先存儲原子屬性
Long id = saveBareBusiNode(node, parentNodeId);
// 再存儲類集屬性
Map collectionPropsMap = node.getCollectionPropsMap();
for (Iterator it = collectionPropsMap.keySet().iterator(); it.hasNext();) {
String attName = (String) it.next();
Collection coll = null;
try {
coll = (Collection) PropertyUtils.getProperty(node, attName);
} catch (Exception e) {
throw new RuntimeException("編碼錯誤");
}
for (Iterator iitt = coll.iterator(); iitt.hasNext();) {
BusiNode subNode = (BusiNode) iitt.next();
saveBusiNode(subNode, id);
}
}
// 最後存儲所有BusiNode屬性
Iterator iitt = node.getBusiNodePropNames().iterator();
while (iitt.hasNext()) {
BusiNode subNode = null;
try {
subNode = (BusiNode) PropertyUtils.getProperty(node,
(String) iitt.next());
} catch (Exception e) {
throw new RuntimeException("編碼錯誤");
}
if (subNode != null) {
saveBusiNode(subNode, id);
}
}
return id;
}
/**
* 插入某個BusiNode的根結點,此方法可能拋出RuntimeException
*
* @param node
* @return
*/
private Long saveBareBusiNode(BusiNode node, Long parentNodeId) {
StringBuffer sbForSql = new StringBuffer();
List paramValues = new ArrayList();
genInsertSqlAndParam(node, parentNodeId, node.getAtomicPropNames(),
sbForSql, paramValues);
return new Long(JdbcUtil.queryForLong(
sbForSql.toString(), paramValues.toArray()));
}
/**
* 生成某個結點的插入語句和paramValues數組,此方法可能拋出RuntimeException
*
* @param node
* @param columnNames
* @param sbForSql
* @param paramValues
*/
private void genInsertSqlAndParam(BusiNode node, Long parentNodeId,
List columnNames, StringBuffer sbForSql, List paramValues) {
sbForSql.append(" insert into ");
sbForSql.append(MyUtils.getClassBareName(node.getClass()));
List cns = new ArrayList();
cns.addAll(columnNames);
cns.add("parentNodeId");
sbForSql.append(MyUtils.encloseWithCurve(MyUtils
.joinCollectionStrings(cns, ",")));
sbForSql.append(" values ");
List qms = new ArrayList(); // 問號
for (Iterator it = columnNames.iterator(); it.hasNext();) {
qms.add("?");
String cn = (String) it.next();
try {
paramValues.add(PropertyUtils.getProperty(node, cn));
} catch (Exception e) {
throw new RuntimeException(e);
}
}
qms.add("?"); // parentNodeId
paramValues.add(parentNodeId);
sbForSql.append(MyUtils.encloseWithCurve(MyUtil
.joinCollectionStrings(qms, ",")));
sbForSql.append(";select @@identity");
}
}