一·、需求

写一个功能类,能将一个给定的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对象的方式已被证明不可行,我考虑在遍历结果集的同时边读边写文件。


使用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的标签不能以数字开头,本例略去了对节点名是否合法的判断。