一·、需求
寫一個功能類,能將一個給定的sql select語句的執行結果集按照一定格式生成xml文件。
比如,一個sql語句"select * from star;"的執行結果是這樣的:
---------------------------
name age gender
---------------------------
James 26 male
Bryant 33 male
要求生成后的xml的根節點名叫"star"且每條數據使用一個<row>標簽來代表,就像下面的這樣:
<?xml version="1.0" encoding="UTF-8" standalone="no" ?>
<star xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<row>
<name>James</name>
<age>26</age>
<gender>male</gender>
</row>
<row>
<name>Bryant</name>
<age>33</age>
<gender>male</gender>
</row>
</star>
二、早期作法
早期分析這個需求的時候,很自然的將需求分成了兩塊功能來完成,一塊用來生成xml,另一塊用來將xml輸出到文件。
1、生成xml:
生成xml的方式的總體思路是,首先通過JDBC連接執行sql得到結果集,然后遍歷結果集將內容填充到一個
org.w3c.dom.Document對象,最后使用javax.xml.transform.Transformer將Document對象轉換成xml。
@Component("xmlMaker")
public class XmlMaker{
private static DataSource datasource;
private static final String XSDNS="http://ww.w3.org/2001/XMLSchema";
private static final String ROW_ELEMENT_NAME="row";
private String content;
@Resource(name="mydatasource")
public void setDataSource(DataSource dataSource){
this.dataSource=datasource;
}
public String generateXML(String sql, String rootElementName){
Connection con = null;
PreparedStatement ps = null;
ResultSet rs = null;
String result = null;
try{
con = dataSource.getConnection();
// 創建一個可滾動的只讀結果集,普通類型的結果集的游標無法自由上下移動
ps = con.prepareStatement(sql,ResultSet.TYPE_SCROLL_INSENSITIVE,ResultSet.CONCUR_READ_ONLY);
rs = ps.executeQuery();
result = makeXMLFromResultSet(rs,rootElementName);
} catch(Exception e){
//todo
}finally{
try{
if(null != rs){
rs.close();
}
if(null != ps){
ps.close();
}
if(null != con){
con.close();
}
}catch(SQLException e){
//todo
}
}
return result;
}
private String makeXMLFromResultSet(ResultSet rs,String rootElementName) throws Exception{
Document doc = resultSet2Dom(rs, rootElementName);
String ret = null;
StringWriter sw = new StringWriter();
Transformer t = null;
try{
TransformerFactory tf = TransformerFactory.newInstance();
t = tf.newTransformer();
t.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no");
t.setOutputProperty(OutputKeys.METHOD, "xml");
t.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
t.setOutputProperty(OutputKeys.INDENT, "yes");
DOMSource domSource = new DOMSource(doc);
StreamResult sr = new StreamResult(sw);
transformer.transform(domSource,sr);
content = sw.toString();
}catch(Exception e){
//todo
}finally{
doc = null;
try{
sw.close();
}catch(IOException e){
}
}
return content;
}
private Document resultSet2Dom(ResultSet rs,String rootElementName){
Document myDocument = null;
try{
myDocument = ((DocumentBuilderFactory.newInstance()).newDocumentBuilder()).newDocument();
}catch(ParserConfigurationException pce){
//todo
}
Element root = myDocument.createElement(rootElementName);
root.setAttribute("xmlns:xsi","http://www.w3.org/2001/XMLSchema-instance");
myDocument.appendChild(root);
ResultSetMetaData rsmd = rs.getMetaData();
Element element,row;
String value;
if(rs.next()){
rs.previous();//使用rs.next()來判斷結果集是否有至少一條數據,如果有就將游標退回初始位置准備開始遍歷。
while(rs.isLast() == false){
rs.next();
row = myDocument.createElement(ROW_ELEMENT_NAME);
root.appendChild(row);
for(int i=1;i<=rsmd.getColumnCount();i++){
element = myDocument.createElement(rsmd.getColumnLabel(i).toLowerCase());
int columnType = rsmd.getColumnType();//此處得到列類型是方便對特殊類型數據的處理,比如當數據是浮點型時四舍五入。本例略
value = rs.getString(i);
if(value == null){
element.setAttribute("xsi:nil","true");
}else{
element.appendChild(myDocument.createTextNode(value));
}
row.appendChild(element);
}
}
return myDocument;
}
}
2、將xml寫成文件
public class FileMaker{
public void static writeFile(String filePath,Sring fileName,String content){
File fileDirectory = new File(filePath);
File targetFile = new File(filePath + File.separator + fileName);
if(!(fileDirectory.isDirectory())){
fileDirectory.mkdirs();//如果傳過來的文件路徑不存在,就先創建這個路徑
}
if(!(targetFile.isFile())){
try{
targetFile.createNewFile();//如果目標文件不存在就創建文件
}catch(IOException e){
//todo
}
}
FileOutputStream fos = null;
try{
fos = new FileOutputStream(targetFile);
org.apache.commons.io.IOUtils.write(content,fos,"UTF-8");
}catch(IOException e){
//todo
}finally{
IOUtils.closeQuietly(fos);
}
}
}
三、遇到問題
在數據量小時,這種做法能正常工作,但有一天別人在使用的時候系統卡死了,Debug后發現在結果集過大(當時有三百萬條數據)時,內存溢出了。因為依照上面的做法,需要將一個有三百萬條數據的結果集轉成一個Dom對象放在內存中。於是我加大內存,終
於挨過了生成xml這個環節,得到了一個龐大的字符串content。但由於Dom對象的引用雖然被指向了null但它之前所占用的內存並不可能立即釋放,所以在寫文件時內存又不夠了,又溢出。其實,加內存並不是解決問題的辦法,因為數據量不固定,這終究是一
個不健壯的程序。
四、修改方案,解決問題
將結果集硬生生的打造成一個大Dom對象的方式已被證明不可行,我考慮在遍歷結果集的同時邊讀邊寫文件。
感謝 http://www.clipclip.org/wqmd/clips/detail/1204498 的作者。
使用SAX的方式在遍歷結果集的同時生成xml文件。
public void resultSet2XML(ResultSet rs, String rootElementName,String filePath) throws Exception{
SAXTransformerFactory fac = (SAXTransformerFactory)SAXTransfomerFactory.newInstance();
TransformerHandler handler = fac.newTransformerHandler();
Transformer transformer = handler.getTransformer();
Transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION,"no");
Transformer.setOutputProperty(OutputKeys.METHOD,"xml");
Transformer.setOutputProperty(OutputKeys.ENCODING,"UTF-8");
Transformer.setOutputProperty(OutputKeys.INDENT,"yes");
Transformer.setOutputProperty(OutputKeys.STANDALONE,"no");
FileOutputStream fos = new FileOutputStream(filePath);
Result resultxml = new StreamResult(fos);
handler.setResult(resultxml);
ResultSetMetaData rsmd = rs.getMetaData();
String value = "";
AttributeImpl rootElementAttribute = new AttributesImpl();
rootElementAttribute.addAttribute("","","xmlns:xsi","","http://www.w3.org/2001/XMLSchema-instance");
handler.startDocument();
handler.startElement("","",rootElementName,rootElementAttribute);
if(rs.next()){
rs.previous();
while(rs.isLast() == false){
rs.next();
handler.startElement("","",ROW_ELEMENT_NAME,null);
for(int i=1;i<=rsmd.getColumnCount();i++){
int columnType = rsmd.getColumnType();
value = rs.getString(i);
String columnName = rsmd.getColumnLabel(i).toLowerCase();
if(value == null){
AttributesImpl tempAttribute = new AttributesImpl();
tempAttribute.addAttribute("","","xsi:nil","","true");
handler.startElement("","",columnName,tempAttribute);
}else{
handler.startElement("","",columnName,null);
}
handler.character(value.toCharArray(),0,value.length());
handler.endElement("","",ROW_ELEMENT_NAME);
}
handler.endElement("","",rootElementName);
handler.endDocument();
fos.close();
}
}
}
上面的方法執行完的同時,文件輸出流fos也完整地結束了寫文件的工作並關閉,從此,再大的結果集我們都不怕了。
BTW:XML的標簽不能以數字開頭,本例略去了對節點名是否合法的判斷。
