Skip to content

6. XML 和 JAVA

在本章中,我们将介绍如何在 Java 中使用 XML 文档。我们将以上一章所研究的税务应用程序为例进行说明。

6.1. XML 文件和 XSL 样式表

请看以下 XML simulations.xml 文件,它可能代表了税务计算模拟的结果:

<?xml version="1.0" encoding="ISO-8859-1"?>
<simulations>
  <simulation marie="oui" enfants="2" salaire="200000" impot="22504"/>
  <simulation marie="non" enfants="2" salaire="200000" impot="33388"/>
</simulations>

如果使用 IE 6 进行查看,将得到以下结果:

Image

IE6 通过文件的 .xml 后缀识别出这是 XML 文件,并以自身特有的方式进行排版。 使用 Netscape 则会显示一个空白页面。然而,如果查看源代码(View/Source),确实可以看到原始的 XML 文件:

Image

为什么 Netscape 没有显示任何内容?因为它需要一个样式表来告诉它如何将 XML 文件转换为 HTML 文件,这样它才能显示出来。 恰巧 IE 6 本身带有默认样式表,而 XML 则没有提供样式表,这正是此处的情况。

有一种名为 XSL(eXtended StyleSheet 语言)的语言,用于描述将 XML 文件转换为任意文本文件所需的转换过程。 XSL支持多种指令,其语法与编程语言极为相似。本文将不作详细阐述,因为这需要数十页的篇幅。 我们仅列举两个 XSL 样式表的示例。第一个样式表用于将 XML 文件转换为 simulations.xml 代码。 我们修改后者,使其指定浏览器可用于将其转换为 HTML 文档的样式表,该文档可被浏览器显示:

<?xml version="1.0" encoding="ISO-8859-1" ?>
<?xml-stylesheet type="text/xsl" href="simulations.xsl"?>
<simulations>
  <simulation marie="oui" enfants="2" salaire="200000" impot="22504"/>
  <simulation marie="non" enfants="2" salaire="200000" impot="33388"/>
</simulations>

订单 XML

<?xml-stylesheet type="text/xsl" href="simulations.xsl"?>

将文件 simulations.xsl 指定为类型为 text/xsl 的样式表(xml-stylesheet)c.a.d。一个包含代码的文本文件 XSL。 浏览器将使用此样式表将文本 XML 转换为文档 HTML。以下是在 Netscape 7 中加载文件 XML simulations.xml 时获得的结果:

Image

当我们查看文档的源代码(View/Source)时,发现的是原始文档 XML,而非显示的文档 HTML:

Image

Netscape 使用样式表 simulations.xsl 将上述文档 XML 转换为可显示的文档 HTML。现在是时候查看该样式表的内容了:

<?xml version="1.0" encoding="ISO-8859-1"?>
  <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
      <xsl:output method="html" indent="yes"/>

    <xsl:template match="/">
        <html>
          <head>
            <title>Simulations de calculs d'impôts</title>
        </head>
          <body>
            <center>
              <h3>Simulations de calculs d'impôts</h3>
            <hr/>
            <table border="1">
                    <th>marié</th><th>enfants</th><th>salaire</th><th>impôt</th>
                          <xsl:apply-templates select="/simulations/simulation"/>
                        </table>
         </center>
       </body>
     </html>
        </xsl:template>


    <xsl:template match="simulation">
          <tr>
            <td><xsl:value-of select="@marie"/></td>
            <td><xsl:value-of select="@enfants"/></td>
            <td><xsl:value-of select="@salaire"/></td>
            <td><xsl:value-of select="@impot"/></td>
        </tr>
       </xsl:template>
  </xsl:stylesheet>
  • 样式表 XSL 是一个 XML 文件,因此遵循其规则。它必须“格式正确”,即所有打开的标签都必须关闭。
  • 该文件以两条命令开头,这些命令可在任何样式表中保留:
<?xml version="1.0" encoding="ISO-8859-1"?>
  <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">

属性 encoding="ISO-8859-1" 允许在样式表中使用带重音的字符。

  • 标签 <xsl:output method="html" indent="yes"/> 指示解析器 XSL 生成“缩进”的 HTML 内容。
  • 标签 <xsl:template match="元素"> 用于定义 XML 文档中将应用 <xsl:template ...> </xsl:template> 之间指令的元素。
    <xsl:template match="/">
................
        </xsl:template>

在上例中,元素“/”表示文档的根节点。这意味着一旦遇到文档 XML 的开头,位于两个标签之间的 XSL 命令就会被执行。

  • 除 XSL 标签外,其余内容将原样写入输出流。XSL 标签会被执行。其中部分标签生成的结果会被写入输出流。让我们来看以下示例:
    <xsl:template match="/">
        <html>
          <head>
            <title>Simulations de calculs d'impôts</title>
        </head>
          <body>
            <center>
              <h3>Simulations de calculs d'impôts</h3>
            <hr/>
            <table border="1">
                    <th>marié</th><th>enfants</th><th>salaire</th><th>impôt</th>
                          <xsl:apply-templates select="/simulations/simulation"/>
                        </table>
          </center>
        </body>
      </html>
        </xsl:template>

需要说明的是,所分析的文档XML如下:

<?xml version="1.0" encoding="ISO-8859-1"?>
<simulations>
  <simulation marie="oui" enfants="2" salaire="200000" impot="22504"/>
  <simulation marie="non" enfants="2" salaire="200000" impot="33388"/>
</simulations>

从所分析的文档 XML 的开头(match="/") 开始,解析器 XSL 将输出文本

<html>
          <head>
            <title>Simulations de calculs d'impôts</title>
        </head>
          <body>
            <center>
              <h3>Simulations de calculs d'impôts</h3>
            <hr>
            <table border="1">
                    <th>marié</th><th>enfants</th><th>salaire</th><th>impôt</th>

需要注意的是,在原始文本中我们使用的是 <hr/>,而不是 <hr>。在原始文本中无法写成 <hr>,因为虽然它是有效的 HTML 标签,但却是无效的 XML 标签。 然而,我们这里处理的是一个必须“格式正确”的文本,即所有标签都必须闭合。 因此我们写成 <hr/>,并且因为我们写了 <xsl:output text="html ...">,解析器会将文本 <hr/> 转换为 <hr>。紧随其后的是由命令生成的文本:

      <xsl:apply-templates select="/simulations/simulation"/>

我们稍后将了解该文本的内容。最后,解释器将添加以下文本:

                    </table>
          </center>
        </body>
      </html>

<xsl:apply-templates select="/simulations/simulation"/> 命令要求执行 /simulations/simulation 元素的“模板”。 每当解析器 XSL 在已解析的文本 XML 中遇到位于 <simulations>..</simulations> 标签内的 <simulation>..</simulations> <simulation/> 标签时,该模板就会被执行。 当遇到 <simulation> 标签时,解释器将执行以下模板中的指令:

    <xsl:template match="simulation">
          <tr>
            <td><xsl:value-of select="@marie"/></td>
            <td><xsl:value-of select="@enfants"/></td>
            <td><xsl:value-of select="@salaire"/></td>
            <td><xsl:value-of select="@impot"/></td>
        </tr>
       </xsl:template>

考虑以下 XML 行:

<simulations>
  <simulation marie="oui" enfants="2" salaire="200000" impot="22504"/>

<simulation ..> 对应于指令 XSL 的模板 <xsl:apply-templates select="/simulations/simulation">。因此,解释器 XSL 将尝试应用与该模板匹配的指令。 它将找到模板 <xsl:template match="simulation"> 并执行它。 需要提醒的是,非 XSL 命令的内容会被 XSL 解释器原样保留,而 XSL 命令则会被其执行结果所替换。 因此,指令 XSL <xsl:value-of select="@champ"/> 被解析节点(此处为节点 <simulation>)的“champ”属性的值所替换。 对前一行 XML 的解析将产生以下输出结果:

XSL
输出
<tr><td>
<tr><td>
<xsl:value-of select="@marie"/>
</td><td>
</td><td>
<xsl:value-of select="@children"/>
2
</td><td>
</td><td>
<xsl:value-of select="@salaire"/>
200000
</td><td>
</td><td>
<xsl:value-of select="@impot"/>
22504
</td></tr>
</td></tr>

总计,行 XML

<simulation marie="oui" enfants="2" salaire="200000" impot="22504"/>

将转换为行 HTML:

<tr><td>oui</td><td>2</td><td>200000</td><td>22504</td></tr>

虽然这些解释略显简略,但读者现在应该能够看出,以下文本 XML:

<?xml version="1.0" encoding="ISO-8859-1"?>
<?xml-stylesheet type="text/xsl" href="simulations.xsl"?>
<simulations>
  <simulation marie="oui" enfants="2" salaire="200000" impot="22504"/>
  <simulation marie="non" enfants="2" salaire="200000" impot="33388"/>
</simulations>

附带以下样式表 XSL simulations.xsl

<?xml version="1.0" encoding="ISO-8859-1"?>
  <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
      <xsl:output method="html" indent="yes"/>

    <xsl:template match="/">
        <html>
          <head>
            <title>Simulations de calculs d'impôts</title>
        </head>
          <body>
            <center>
              <h3>Simulations de calculs d'impôts</h3>
            <hr/>
            <table border="1">
                    <th>marié</th><th>enfants</th><th>salaire</th><th>impôt</th>
                          <xsl:apply-templates select="/simulations/simulation"/>
                        </table>
         </center>
       </body>
     </html>
        </xsl:template>


    <xsl:template match="simulation">
          <tr>
            <td><xsl:value-of select="@marie"/></td>
            <td><xsl:value-of select="@enfants"/></td>
            <td><xsl:value-of select="@salaire"/></td>
            <td><xsl:value-of select="@impot"/></td>
        </tr>
       </xsl:template>
  </xsl:stylesheet>

生成以下文本 HTML:

<html>
<head>
<title>Simulations de calculs d'impôts</title>
</head>
<body>
<center>
<h3>Simulations de calculs d'impots</h3>
<hr>
<table border="1">
<th>marié</th><th>enfants</th><th>salaire</th><th>impôt</th>
<tr>
<td>oui</td><td>2</td><td>200000</td><td>22504</td>
</tr>
<tr>
<td>non</td><td>2</td><td>200000</td><td>33388</td>
</tr>
</table>
</center>
</body>
</html>

文件 XML simulations.xml 配合样式表 simulations.xsl,在最新浏览器(此处为 Netscape 7)中显示如下:

Image

6.2. 税务应用程序:第 6 版

6.2.1. 税务应用程序的文件 XML 和样式表 XSL

让我们回到税务Web应用程序,对其进行修改,使返回给客户端的响应采用XML格式,而非HTML格式。 该 XML 响应将附带一个 XSL 样式表,以便浏览器能够显示它。在上段中,我们介绍了:

  • 文件 simulations.xml,它是响应 XML 的原型,其中包含税款计算的模拟
  • 文件 simulations.xsl,该文件将作为样式表 XSL,随附于响应文件 XML

我们还需考虑包含错误的响应情况。在此情况下,响应原型 XML 将为以下文件 erreurs.xml

<?xml version="1.0" encoding="windows-1252"?>
<?xml-stylesheet type="text/xsl" href="erreurs.xsl"?>
<erreurs>
    <erreur>erreur 1</erreur>
  <erreur>erreur 2</erreur>
</erreurs>

用于在浏览器中显示此文档 XML 的样式表 erreurs.xsl 如下:

<?xml version="1.0" encoding="windows-1252"?>
  <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
      <xsl:output method="html" indent="yes"/>

    <xsl:template match="/">
        <html>
          <head>
            <title>Simulations de calculs d'impôts</title>
        </head>
          <body>
            <center>
              <h3>Simulations de calculs d'impôts</h3>
          </center>            
          <hr/>
          Les erreurs suivantes se sont produites :
          <ul>
                  <xsl:apply-templates select="/erreurs/erreur"/>
              </ul>            
        </body>
      </html>
        </xsl:template>

    <xsl:template match="erreur">
          <li><xsl:value-of select="."/></li>
        </xsl:template>
</xsl:stylesheet>    

该样式表引入了一个尚未出现的 XSL 指令:<xsl:value-of select="."/>。该指令的输出结果为所解析节点的值,此处为 <erreur>texte</erreur> 节点。 该节点的值即为两个起始标签和结束标签之间的文本,此处为 texte

代码 erreurs.xml 通过样式表 erreurs.xsl 转换为以下文档 HTML:

<html>
<head>
<title>Simulations de calculs d'impots</title>
</head>
<body>
<center>
<h3>Simulations de calculs d'impots</h3>
</center>
<hr>
          Les erreurs suivantes se sont produites :
<ul>
<li>erreur 1</li>
<li>erreur 2</li>
</ul>
</body>
</html>

文件 erreurs.xml 及其样式表在浏览器中的显示效果如下:

Image

6.2.2. xmlsimulations Servlet

我们创建一个名为 index.html 的文件,并将其放置在 impots 应用程序目录中。显示的页面如下:

Image

该 HTML 文档是一个静态文档。其代码如下:

<html>
    <head>
      <title>impots</title>
    <script language="JavaScript" type="text/javascript">
        function effacer(){
           // 清空表单
        with(document.frmImpots){
            optMarie[0].checked=false;
          optMarie[1].checked=true;
          txtEnfants.value="";
          txtSalaire.value="";
          txtImpots.value="";
        }//with
      }//清除

      function calculer(){
           // 在将参数发送至服务器前进行验证
        with(document.frmImpots){
          //子女数量
          champs=/^\s*(\d+)\s*$/.exec(txtEnfants.value);
          if(champs==null){
            // 未验证表单
            alert("Le nombre d'enfants n'a pas été donné ou est incorrect");
            nbEnfants.focus();
            return;
          }//if
          //工资
          champs=/^\s*(\d+)\s*$/.exec(txtSalaire.value);
          if(champs==null){
            // 未验证模板
            alert("Le salaire n'a pas été donné ou est incorrect");
            salaire.focus();
            return;
          }//if
          // 没问题 - 发送
          submit();
        }//附带
      }//计算        
        </script>
  </head>

  <body background="/impots/images/standard.jpg">
      <center>
        Calcul d'impôts
        <hr>
      <form name="frmImpots" action="/impots/xmlsimulations" method="POST">
          <table>
            <tr>
              <td>Etes-vous marié(e)</td>
            <td>
                <input type="radio" name="optMarie" value="oui">oui
              <input type="radio" name="optMarie" value="non" checked>non
            </td>
          </tr>
          <tr>
              <td>Nombre d'enfants</td>
            <td><input type="text" size="3" name="txtEnfants" value=""></td>
          </tr>
          <tr>
              <td>Salaire annuel</td>
            <td><input type="text" size="10" name="txtSalaire" value=""></td>
          </tr>
          <tr></tr>
          <tr>
              <td><input type="button" value="Calculer" onclick="calculer()"></td>
            <td><input type="button" value="Effacer" onclick="effacer()"></td>
          </tr>
        </table>
      </form>
    </center>
 </body>
</html>

请注意,表单数据已提交至 URL /impots/xmlsimulations。该应用程序是一个 Java Servlet,在应用程序 impots 的文件 web.xml 中配置如下:

<?xml version="1.0" encoding="ISO-8859-1"?>

<!DOCTYPE web-app
    PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
    "http://java.sun.com/dtd/web-app_2_3.dtd">

<web-app>
...........
    <servlet>
      <servlet-name>xmlsimulations</servlet-name>
    <servlet-class>xmlsimulations</servlet-class>
    <init-param>
          <param-name>xslSimulations</param-name>
        <param-value>simulations.xsl</param-value>
    </init-param>
    <init-param>
          <param-name>xslErreurs</param-name>
        <param-value>erreurs.xsl</param-value>
    </init-param>

    <init-param>
          <param-name>DSNimpots</param-name>
        <param-value>mysql-dbimpots</param-value>
    </init-param>
    <init-param>
          <param-name>admimpots</param-name>
        <param-value>admimpots</param-value>
    </init-param>
    <init-param>
          <param-name>mdpimpots</param-name>
        <param-value>mdpimpots</param-value>
    </init-param>    
  </servlet>
........
  <servlet-mapping>
      <servlet-name>xmlsimulations</servlet-name>
    <url-pattern>/xmlsimulations</url-pattern>
  </servlet-mapping>
</web-app>
  • 该 Servlet 名为 xmlsimulations,并基于类 xmlsimulations.class
  • 其参数包括 DSNimpotsadmimpots mdpimpots,这些参数用于访问税务数据库。此外,它还支持另外两个参数:
    • xslSimulations,即必须随响应文件XML(该文件包含模拟结果)一同提供的样式文件名称
    • xslErreurs,这是必须随附在包含可能错误的响应 XML 中的样式文件名称
  • 它有一个别名 xmlsimulations,可通过 URL http://localhost:8080/impots/xmlsimulations 访问。

xmlsimulations Servlet 的框架与之前研究过的 simulations Servlet 类似。 主要区别在于它需要生成 XML 而不是 HTML。这将导致删除之前应用程序中使用的 JSP 文件。 这些文件的主要作用是提高生成的 HTML 代码的可读性,避免其淹没在 Servlet 的 Java 代码中。现在,这一作用已不再必要。该 Servlet 需要生成两种类型的 XML 代码:

  • 用于模拟的代码
  • 用于错误处理的代码

此前我们已介绍并研究了这两种情况下应提供的 XML 响应类型,以及应随附的样式表。Servlet 的代码如下:

import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;
import java.util.regex.*;
import java.util.*;

public class xmlsimulations extends HttpServlet{

    // 实例变量
    String msgErreur=null;
    String xslSimulations=null;
    String xslErreurs=null;
    String DSNimpots=null;
    String admimpots=null;
    String mdpimpots=null;
    impotsJDBC impots=null;

    //-------- GET
    public void doGet(HttpServletRequest request, HttpServletResponse response)
    throws IOException, ServletException{

        // 获取发往客户端的写入流
        PrintWriter out=response.getWriter();

        // 指定响应类型
        response.setContentType("text/xml");

         // 错误列表
        ArrayList erreurs=new ArrayList();

         // 初始化是否成功?
        if(msgErreur!=null){
             // 完成 - 将包含错误的响应发送至服务器
            erreurs.add(msgErreur);
            sendErreurs(out,xslErreurs,erreurs);
             // 完成
            return;
        }

         // 检索本会话的先前模拟
        HttpSession session=request.getSession();
        ArrayList simulations=(ArrayList)session.getAttribute("simulations");
        if(simulations==null) simulations=new ArrayList();

        // 正在检索当前请求的参数
        String optMarie=request.getParameter("optMarie");              // 婚姻状况
        String txtEnfants=request.getParameter("txtEnfants");       // 子女数量
        String txtSalaire=request.getParameter("txtSalaire");       // 年薪

         // 是否已获取所有预期参数
        if(optMarie==null || txtEnfants==null || txtSalaire==null){
             // 缺少参数
             // 发送带错误的响应
            erreurs.add("Demande incomplète. Il manque des paramètres");
            sendErreurs(out,xslErreurs,erreurs);
             // 处理完毕
            return;
        }

         // 已获取所有参数 - 正在验证
         // 婚姻状况
        if( ! optMarie.equals("oui") && ! optMarie.equals("non")){
             // 错误
            erreurs.add("Etat marital incorrect");
        }
         // 子女数量
        txtEnfants=txtEnfants.trim();
        if(! Pattern.matches("^\\d+$",txtEnfants)){
            // 错误
            erreurs.add("Nombre d'enfants incorrect");
        }
         // 工资
        txtSalaire=txtSalaire.trim();
        if(! Pattern.matches("^\\d+$",txtSalaire)){
            // 错误
            erreurs.add("Salaire incorrect");
        }

        if(erreurs.size()!=0){
            // 如有错误,将予以报告
            sendErreurs(out,xslErreurs,erreurs);
        }else{
             // 无错误
            try{
                 // 可以计算应缴税款
                int nbEnfants=Integer.parseInt(txtEnfants);
                int salaire=Integer.parseInt(txtSalaire);
                String txtImpots=""+impots.calculer(optMarie.equals("oui"),nbEnfants,salaire);
                 // 将当前结果添加到之前的模拟中
                String[] simulation={optMarie.equals("oui") ? "oui" : "non",txtEnfants, txtSalaire, txtImpots};
                simulations.add(simulation);
                 // 发送包含模拟结果的回复
                sendSimulations(out,xslSimulations,simulations);
            }catch(Exception ex){}
        }//if-else
         // 将模拟列表重新放入会话中
        session.setAttribute("simulations",simulations);
    }//GET

     //-------- POST
    public void doPost(HttpServletRequest request, HttpServletResponse response)
    throws IOException, ServletException{
        doGet(request,response);
    }//POST

     //-------- INIT
    public void init(){
         // 正在读取初始化参数
        ServletConfig config=getServletConfig();
        xslSimulations=config.getInitParameter("xslSimulations");
        xslErreurs=config.getInitParameter("xslErreurs");
        DSNimpots=config.getInitParameter("DSNimpots");
        admimpots=config.getInitParameter("admimpots");
        mdpimpots=config.getInitParameter("mdpimpots");

         // 参数正确吗?
        if(xslSimulations==null || DSNimpots==null || admimpots==null || mdpimpots==null){
            msgErreur="Configuration incorrecte";
            return;
        }

         // 创建 impotsJDBC 的实例
        try{
            impots=new impotsJDBC(DSNimpots,admimpots,mdpimpots);
        }catch(Exception ex){
            msgErreur=ex.getMessage();
        }
    }//初始化

     //-------- sendErreurs
    private void sendErreurs(PrintWriter out,String xslErreurs,ArrayList erreurs){
        String réponse="<?xml version=\"1.0\" encoding=\"windows-1252\"?>"
                    + "<?xml-stylesheet type=\"text/xsl\" href=\""+xslErreurs+"\"?>\n"
              +"<erreurs>\n";
        for(int i=0;i<erreurs.size();i++){
            réponse+="<erreur>"+(String)erreurs.get(i)+"</erreur>\n";
        }//用于
        réponse+="</erreurs>\n";
         // 发送响应
        out.println(réponse);
    }

     //-------- sendSimulations
    private void sendSimulations(PrintWriter out, String xslSimulations, ArrayList simulations){
        String réponse="<?xml version=\"1.0\" encoding=\"windows-1252\"?>"
                    + "<?xml-stylesheet type=\"text/xsl\" href=\""+xslSimulations+"\"?>\n"
              + "<simulations>\n";
        String[] simulation=null;
        for(int i=0;i<simulations.size();i++){
            // 模拟编号 i
            simulation=(String[])simulations.get(i);
            réponse+="<simulation "
                     +"marie=\""+(String)simulation[0]+"\" "
                     +"enfants=\""+(String)simulation[1]+"\" "
                     +"salaire=\""+(String)simulation[2]+"\" "
                 +"impot=\""+(String)simulation[3]+"\" />\n";
        }//用于
        réponse+="</simulations>\n";
         // 发送响应
        out.println(réponse);
    }
}

让我们详细说明该代码与我们已知内容相比的主要新功能:

  • init 过程从配置文件 web.xml 中获取了新参数:响应中必须包含的两份样式表 XSL 的名称被分别存入变量 xslSimulations xslErreurs 中。 这两个样式表即为之前分析过的文件 simulations.xsl erreurs.xsl。它们被放置在 impots 应用程序的目录中:
dos>dir E:\data\serge\Servlets\impots\*.xsl
27/08/2002  08:15                1 030 simulations.xsl
27/08/2002  09:23                  795 erreurs.xsl
  • 过程 GET 首先检查初始化过程中是否发生错误。如果发生错误,则调用过程 sendErreurs,该过程会生成适用于此情况的响应 XML,然后终止。 在该响应中,会插入指定要使用的样式表的指令。
  • 如果未发生错误,则 GET 过程将分析客户端请求的参数。若发现任何错误,它将通过调用 sendErreurs 过程进行报告。 否则,它将计算新的模拟结果,将其添加到当前会话中存储的旧结果中,并通过 sendSimulations 过程发送其响应 XML。后者与 sendErreurs 过程的操作方式类似。
  • 值得注意的是,Servlet将响应声明为text/xml类型:
         // 指定响应类型
        response.setContentType("text/xml");

以下是执行示例。初始表单的填写方式如下:

Image

由于未启动数据库 MySQL,导致无法在 Servlet 的 init 方法中构建 impots 对象。因此,Servlet 的响应如下:

Image

浏览器接收到的代码(查看源代码)如下:

Image

现在,在启动数据库后重新进行两次模拟,结果如下:

Image

此次浏览器接收到的代码如下:

Image

值得注意的是,由于删除了文件 JSP,我们的新应用程序比以前更简单了。 这些页面所承担的部分工作已转移至样式表 XSL。这种新的任务分配方式的优势在于:一旦确定了 Servlet 响应的 XML 格式,样式表的开发便与 Servlet 的开发相互独立。

6.3. 使用Java分析XML文档

我们的 impots 应用程序的第 7 版和第 8 版将作为之前 xmlsimulations Servlet 的编程客户端。它们将接收 XML 格式的代码,并需要对其进行解析以提取所需的信息。 在此,我们将暂且搁置各版本的开发,学习如何使用 Java 解析 XML 文档。我们将基于 JBuilder 7 版本随附的一个名为 MySaxParser 的示例进行演示。该程序的调用方式如下:

dos>java MySaxParser
Usage: java MySaxParser [URI]

MySaxParser 应用程序接受一个参数:待分析文档 XML 的 URI(统一资源标识符)。 在本例中,该 URI 仅为放置在应用程序目录中的 XML 文件的名称。下面看两个运行示例。 在第一个示例中,被分析的 XML 文件是 erreurs.xml 文件:

<?xml version="1.0" encoding="ISO-8859-1"?>
<?xml-stylesheet type="text/xsl" href="erreurs.xsl"?>
<erreurs>
    <erreur>erreur 1</erreur>
  <erreur>erreur 2</erreur>
</erreurs>

分析结果如下:

dos> java MySaxParser erreurs.xml
Début du document
Début élément <erreurs>
Début élément <erreur>
[erreur 1]
Fin élément <erreur>
Début élément <erreur>
[erreur 2]
Fin élément <erreur>
Fin élément <erreurs>
Fin du document

我们之前尚未说明MySaxParser应用程序的功能,但在此可以看到,它显示了已分析文档XML的结构。 第二个示例分析了文件 XML simulations.xml

<?xml version="1.0" encoding="ISO-8859-1"?>
<?xml-stylesheet type="text/xsl" href="simulations.xsl"?>
<simulations>
  <simulation marie="oui" enfants="2" salaire="200000" impot="22504"/>
  <simulation marie="non" enfants="2" salaire="200000" impot="33388"/>
</simulations>

分析结果如下:

dos>java MySaxParser simulations.xml
Début du document
Début élément <simulations>
Début élément <simulation>
marie = oui
enfants = 2
salaire = 200000
impot = 22504
Fin élément <simulation>
Début élément <simulation>
marie = non
enfants = 2
salaire = 200000
impot = 33388
Fin élément <simulation>
Fin élément <simulations>
Fin du document

MySaxParser 包含了我们在 impots 应用程序中所需的一切,因为它能够检索 Web 服务器可能发送的错误或模拟数据。让我们来看看它的代码:

import java.io.IOException;
import org.xml.sax.*;
import org.xml.sax.helpers.*;
import org.apache.xerces.parsers.SAXParser;
import java.util.regex.*;

// 类
public class MySaxParser extends DefaultHandler {

     // 树中元素的值XML
    private StringBuffer valeur=new StringBuffer();
    // 当需要忽略某个元素的值时,其正则表达式
     // 忽略其前后的“空格”
    private static Pattern ptnValeur=null;
    private static Matcher résultats=null;

    // -------- 主
    public static void main(String[] argv) {
        // 验证参数数量
        if (argv.length != 1) {
            System.out.println("Usage: java MySaxParser [URI]");
            System.exit(0);
        }
         // 从待分析的文件中提取URI
        String uri = argv[0];
        try {
            // 创建 XML 解析器
            XMLReader parser = XMLReaderFactory.createXMLReader("org.apache.xerces.parsers.SAXParser");
             // 向解析器指定将实现这些方法的对象
             // startDocument, endDocument, startElement, endElement, 字符
            MySaxParser MySaxParserInstance = new MySaxParser();
            parser.setContentHandler(MySaxParserInstance);
             // 初始化元素的值模板
            ptnValeur=Pattern.compile("^\\s*(.*?)\\s*$");
             // 告知解析器要分析的文档为 XML
            parser.parse(uri);
        }
        catch(Exception ex) {
             // 错误
            System.err.println("Erreur : " + ex);
             // 日志
            ex.printStackTrace();
        }
    }//main


     // -------- startDocument
    public void startDocument() throws SAXException {
         // 解析器遇到文档开头时调用的过程
        System.out.println("Début du document");
    }//startDocument

     // -------- endDocument
    public void endDocument() throws SAXException {
         // 解析器遇到文档结尾时调用的过程

        System.out.println("Fin du document");
    }//endDocument

     // -------- startElement
    public void startElement(String uri, String localName, String qName,
        Attributes attributes) throws SAXException {
         // 解析器遇到标签开头时调用的过程
         // uri:被解析文档的 URI?
         // localName:当前正在解析的元素名称
         // qName:同上,但若存在命名空间则由其“限定”
         // attributes:元素的属性列表

         // 后续
        System.out.println("Début élément <"+localName+">");
         // 该元素是否有属性?
        for (int i = 0; i < attributes.getLength(); i++) {
            System.out.println(attributes.getLocalName(i) + " = " + attributes.getValue(i));
        }//用于
    }//startElement

    // -------- 字符
    public void characters(char[] ch, int start, int length) throws SAXException {
        // 解析器遇到文本时反复调用的过程
         // 位于两个 <标签>文本</标签> 标签之间
         // 文本位于 ch 中,从 start 字符开始,长度为 length 个字符

         // 该文本被添加到缓冲区值中
        valeur.append(ch, start, length);
    }//字符

        // -------- endElement
    public void endElement(String uri, String localName, String qName)
            throws SAXException {
         // 解析器遇到标签结束时调用的过程
         // URI:URI 分析文档的?
         // localName:当前正在解析的元素名称
         // qName:同上,但若存在命名空间则由其“限定”

         // 显示元素的值
        String strValeur=valeur.toString();
        if (ptnValeur==null) System.out.println("null");
        résultats=ptnValeur.matcher(strValeur);
        if (résultats.find() && ! résultats.group(1).equals("")){
            System.out.println("["+résultats.group(1)+"]");
        }//if
         // 将元素值设为空
        valeur.setLength(0);

         // 随后
        System.out.println("Fin élément <"+localName+">");
    }//endElement

}//分类

首先,让我们定义一个在XML文档分析中频繁出现的缩写:SAX,其全称为Simple Api for Xml。这是一组Java类,旨在简化XML文档的处理工作。 API有两个版本:SAX1和SAX2。上述应用程序使用的是API和SAX2。

该应用程序导入了若干包:

import org.xml.sax.*;
import org.xml.sax.helpers.*;
import org.apache.xerces.parsers.SAXParser;

前两个随 JDK 1.4 版本一同提供,而第三个则不包含在内。 xerces.jar 软件包可在 Apache Web 服务器网站上获取。它不仅随 JBuilder 7 版本提供,还随 Tomcat 4.x 版本提供:

Image

因此,如果希望在不使用 JBuilder 7 的情况下编译上述应用程序,且已拥有 JDK 1.4 以及 Tomcat 4.x,则可编写如下代码:

dos>javac -classpath ".;E:\Program Files\Apache Tomcat 4.0\common\lib\xerces.jar" MySaxParser.java

运行时,操作方式相同:

dos>java -classpath ".;E:\Program Files\Apache Tomcat 4.0\common\lib\xerces.jar" MySaxParser simulations.xml

MySaxParser 继承自类 DefaultHandler。我们稍后会再讨论这一点。现在让我们研究一下过程 main 的代码:

         // 从待分析的文件 XML 中获取 URI
        String uri = argv[0];
        try {
            // 创建 XML 解析器
            XMLReader parser = XMLReaderFactory.createXMLReader("org.apache.xerces.parsers.SAXParser");
             // 向解析器指定将实现这些方法的对象
             // startDocument, endDocument, startElement, endElement, 字符
            MySaxParser MySaxParserInstance = new MySaxParser();
            parser.setContentHandler(MySaxParserInstance);
             // 初始化元素的值模板
            ptnValeur=Pattern.compile("^\\s*(.*?)\\s*$");
             // 向解析器指定待分析的文档 XML
            parser.parse(uri);
        }
        catch(Exception ex) {
             // 错误
            System.err.println("Erreur : " + ex);
             // 日志
            ex.printStackTrace();
        }

要解析文档 XML,我们的应用程序需要一个名为“解析器”的 XML 代码解析器。

            XMLReader parser = XMLReaderFactory.createXMLReader("org.apache.xerces.parsers.SAXParser");

所使用的解析器 XML 由 xerces.jar 软件包提供。获取到的对象类型为 XMLReader。XMLReader 是一个接口,我们在此使用了其中的两个方法:

void setContentHandler(ContentHandler handler)
用于告知解析器,ContentHandler 类型的对象将处理其在解析 XML 文档时生成的事件
void parse(InputSource input)
开始解析作为参数传递的文档 XML

当解析器解析文档 XML 时,它将触发诸如:“遇到文档开头、标签开头、标签属性、标签内容、标签结尾、文档结尾……”等事件。 它将这些事件传递给所传入的 ContentHandler 对象。ContentHandler 是一个接口,定义了用于处理解析器 XML 可能生成的所有事件所需实现的方法。 DefaultHandler 是一个对这些方法进行默认实现的类。DefaultHandler 中实现的方法虽然不执行任何操作,但它们确实存在。当需要通过以下语句告知解析器哪个对象将处理其生成的事件时:

void setContentHandler(ContentHandler handler)

,此时将一个 DefaultHandler 类型的对象作为参数传递会比较方便。如果仅止于此,解析器的任何事件都不会被处理,但我们的程序在语法上仍是正确的。 实际上,我们会向解析器传递一个继承自 DefaultHandler 类的对象,并在其中重写了仅处理我们感兴趣的事件的方法。此处即采用了这种做法:

             // 向解析器指定将实现方法的对象
             // startDocument, endDocument, startElement, endElement, 字符
            MySaxParser MySaxParserInstance = new MySaxParser();
            parser.setContentHandler(MySaxParserInstance);
             // 指示解析器分析文档 XML
            parser.parse(uri);

我们将类 mySaxParser 的一个实例传递给解析器,该类即为我们的类,并在上面通过以下声明定义:

public class MySaxParser extends DefaultHandler {

,并开始分析作为参数传入的文档 URI。至此,对文档 XML 的分析正式开始。 解析器会触发事件,并针对每个事件调用负责处理这些事件的对象(此处为我们的 MySaxParser 对象)中的特定方法。该对象处理五个特定事件,其余事件则被忽略:

解析器触发的事件
处理方法
début du document    
void startDocument()
fin du document
void endDocument()
début d'un élément :<balise>
public void startElement(String uri, String localName, String qName, Attributes attributes)
uri:?
localName:已解析元素的名称。如果遇到的元素是 <simulations>,则 localName="simulations"。
qName:被解析元素的命名空间限定名称。一个 XML 文档可以定义一个命名空间,例如 XX。 因此,前一个标签的限定名称将为 XX:simulations。
attributes:标签的属性列表
valeur d'un élément :
<balise>valeur</balise>
public void characters(char[] ch, int start, int length)
ch:字符数组
start:在字符表 ch 中要使用的第一个字符的索引
length:从 ch 数组中提取的字符数
方法 characters 可以被反复调用。为了构建元素的值,此时会使用一个缓冲区,该缓冲区:
  • 在元素开始时清空
  • 在每次调用方法 characters 时进行填充
  • 在元素结束时清空
fin d'un élément : </balise> ou <balise .../>
void endElement(String uri, String localName, String qName)
参数与方法 startElement 相同。

方法 startElement 允许通过类型为 Attributesattributes 参数获取元素的属性:

  • 属性数量可在 attributes.getLength() 中获取
  • 属性 i 的名称可在 attributes.getLocalName(i) 中找到
  • 属性 i 的值可在 attributes.getValue(i) 中找到
  • localName 属性的值可在 attributes.getValue(localName) 中找到

解释完这一点后,前面的程序及其运行示例便不言自明。我们使用正则表达式来提取元素的值,以便将如下所示的文本 XML:

<erreur>
    erreur 1
</erreur>

将与标签 <erreur> 关联的值处理为“错误 1”,并去除了其前后可能存在的空格和换行符。

6.4. 税务应用程序:第 7 版

现在我们已具备所有要素,可以编写针对税务服务(该服务输出 XML)的客户端程序。我们采用应用程序的第 4 版来构建客户端,并保留第 6 版作为服务器端。在此客户端-服务器应用程序中:

  • 税款计算模拟服务由 servlet xmlsimulations 提供。因此,服务器的响应格式为 XML,正如我们在第 6 版中所见。
  • 客户端不再是浏览器,而是一个独立的Java客户端。其图形界面采用第4版的样式。

以下是几个运行示例。首先是一个错误案例:客户端向 xmlsimulations Servlet 发起请求,但由于 SGBD 和 MySQL 未被启动,该 Servlet 无法正确初始化:

Image

启动 MySQL 并进行一些模拟:

Image

该新版本的客户端与第 4 版客户端的唯一区别在于处理服务器响应的方式。其他方面均未改变。在第 4 版中,客户端接收的是 HTML 代码,它会使用正则表达式从中提取所需的信息。 在此,客户端接收代码 XML,并借助解析器 XML 从其中提取所需信息。

让我们回顾一下我们客户端第4版中“计算”菜单相关流程的概要,因为主要变更就在这里:

  void mnuCalculer_actionPerformed(ActionEvent e) {
....
    try{
             // 计算税款
            calculerImpots(urlImpots,rdOui.isSelected(),nbEnfants.intValue(),salaire);
    }catch (Exception ex){
             // 显示错误
            JOptionPane.showMessageDialog(this,"L'erreur suivante s'est produite : " + ex.getMessage(),"Erreur",JOptionPane.ERROR_MESSAGE);
        }
....
    }//mnuCalculer_actionPerformed

    public void calculerImpots(URL urlImpots,boolean marié, int nbEnfants, int salaire)
          throws Exception{
         // 计算税款
         // urlImpots:税务局的 URL
         // 已婚:已婚为true,否则为false
         // nbEnfants:子女数量
         // 工资:年薪

         // 从urlImpots中提取连接税务服务器所需的信息
....
        try{
             // 连接至服务器
....

             // 创建客户TCP的收支流
....

             // 请求URL - 发送报头 HTTP
....

             // 读取响应的第一行
....
             // 读取响应直至标头结束,并查找可能存在的cookie
            while((réponse=IN.readLine())!=null){
....            }//while

            // 处理完 HTTP 头部后 - 转到代码 HTML
             // 以获取模拟结果
            ArrayList listeSimulations=getSimulations(IN,OUT,simulations);
            simulations.clear();
            for (int i=0;i<listeSimulations.size();i++){
                simulations.addElement(listeSimulations.get(i));
            }

            // 结束
....
    }//calculerImpots

    private ArrayList getSimulations(BufferedReader IN, PrintWriter OUT, DefaultListModel simulations) throws Exception{
....
    }

所有这些代码在新版本中仍然有效。只需将服务器响应 HTML 的处理(上文框选部分)及其显示替换为服务器响应 XML 的处理及其显示:

             // 标题部分已结束 HTTP - 切换至代码 XML
             // 用于检索模拟结果或错误
            ImpotsSaxParser parseur=new ImpotsSaxParser(IN);
            ArrayList listeErreurs=parseur.getErreurs();
            ArrayList listeSimulations=parseur.getSimulations();
             // 关闭与服务器的连接
            client.close();

             // 清理显示列表
            simulations.clear();
             // 错误
            if(listeErreurs.size()!=0){
                 // 将所有错误合并
                String msgErreur="Le serveur a signalé les erreurs suivantes :\n";
                for(int i=0;i<listeErreurs.size();i++){
                    msgErreur+=" - "+(String)listeErreurs.get(i);
                }
                 // 显示错误
                throw new Exception(msgErreur);
            }//if

            // 模拟
            for (int i=0;i<listeSimulations.size();i++){
                simulations.addElement(listeSimulations.get(i));
            }
            return;

上面的代码片段做什么?

  • 它创建了一个名为 XML 的解析器,并向其传递数据流 IN,该数据流包含服务器发送的代码 XML。 该数据流还包含 HTTP 头部信息,但这些信息已被读取并处理完毕。因此,响应中仅剩下 XML 部分。 解析器会生成两组字符串列表:若存在错误则生成错误列表,否则生成模拟列表。这两组列表互斥。
  • 如果错误列表不为空,则将列表中的消息拼接成一条错误消息,并以此消息作为参数抛出异常。该异常将在调用 calculerImpotsmnuCalculer_actionPeformed 过程 中显示。
  • 如果模拟列表不为空,则会在图形界面的 jList 组件中显示该列表。

现在让我们来了解服务器响应解析器 XML,该解析器直接源于我们之前关于如何使用 Java 解析 XML 文档的研究:

import java.io.IOException;
import org.xml.sax.*;
import org.xml.sax.helpers.*;
import org.apache.xerces.parsers.SAXParser;
import java.util.regex.*;
import java.io.*;
import java.util.*;
import javax.swing.*;

// 类
public class ImpotsSaxParser extends DefaultHandler {

    // 树中元素的值XML
    private StringBuffer valeur=new StringBuffer();
    // 当需要忽略前后的“空格”时,元素值的正则表达式
     // 忽略其前后的“空格”
    private Pattern ptnValeur=null;
    private Matcher résultats=null;
     // 元素列表 XML
    private ArrayList listeSimulations=new ArrayList();
    private ArrayList listeErreurs=new ArrayList();
     // XML 部件
    private ArrayList éléments=new ArrayList();
    String élément="";

     // -------- 构建器
    public ImpotsSaxParser(BufferedReader IN) throws Exception{
        // 创建解析器 XML
        XMLReader parser = XMLReaderFactory.createXMLReader("org.apache.xerces.parsers.SAXParser");
         // 向解析器指定将实现方法的对象
         // startDocument, endDocument, startElement, endElement, 字符
        parser.setContentHandler(this);
        // 初始化元素的值模板
        ptnValeur=Pattern.compile("^\\s*(.*?)\\s*$");
         // 起初没有当前的 XML 元素
        éléments.add("");
         // 解析文档
        parser.parse(new InputSource(IN));
    }//生成器

     // -------- startElement
    public void startElement(String uri, String localName, String qName,
        Attributes attributes) throws SAXException {
         // 解析器遇到标签开头时调用的过程
         // URI:URI 分析文档的?
         // localName:当前正在解析的元素名称
         // qName:同上,但若存在命名空间则由其“限定”
         // attributes:元素的属性列表

         // 记录元素名称
        élément=localName.toLowerCase();
        éléments.add(élément);
         // 该元素是否有属性?
        if(élément.equals("simulation") && attributes.getLength()==4){
            // 这是一次模拟——获取属性
            String simulation=attributes.getValue("marie")+","+
                                         attributes.getValue("enfants")+","+
                                         attributes.getValue("salaire")+","+
                                         attributes.getValue("impot");
             // 将模拟添加到模拟列表中
            listeSimulations.add(simulation);
        }//if
    }//startElement

    // -------- 字符
    public void characters(char[] ch, int start, int length) throws SAXException {
        // 解析器遇到文本时反复调用的过程
         // 位于两个 <标签>文本</标签> 标签之间
         // 文本位于 ch 中,从 start 字符开始,长度为 length 个字符

         // 如果属于错误元素,则将文本添加到缓冲区 s 中
        if (élément.equals("erreur"))
           valeur.append(ch, start, length);
    }//字符

        // -------- endElement
    public void endElement(String uri, String localName, String qName)
            throws SAXException {
         // 解析器遇到标签结束时调用的过程
         // uri:被解析文档的 URI ?
         // localName:当前正在解析的元素名称
         // qName:同上,但若存在命名空间则由其“限定”

         // 错误情况
        if(élément.equals("erreur")){
             // 获取错误元素的值
            String strValeur=valeur.toString();
             // 去除其中多余的“空格”,并将其记录在
             // 若列表不为空,则将其保存
            résultats=ptnValeur.matcher(strValeur);
            if (résultats.find() && ! résultats.group(1).equals("")){
                listeErreurs.add(résultats.group(1));
            }//if
        }
         // 将该元素的值设为空
        valeur.setLength(0);
         // 重置元素名称
        éléments.remove(éléments.size()-1);
        élément=(String)éléments.get(éléments.size()-1);
    }//endElement

     // --------- getErreurs
    public ArrayList getErreurs(){
        return listeErreurs;
    }

     // --------- getSimulations
    public ArrayList getSimulations(){
        return listeSimulations;
    }

}//类
  • 生成器接收待分析的数据流 XML IN,并立即进行分析。 分析完成后,对象已构建,并且错误列表(listeErreurs)、模拟列表(listeSimulations)以及结果列表ArrayList)也已构建。 接下来,生成对象的程序只需通过方法 getErreurs getSimulations 获取这两个列表。
  • 在此,我们仅关注由解析器 XML 生成的三个事件:
    • XML 元素的开始,该事件将由 startElement 过程处理。 该过程将处理标签 <simulation marie=".." enfants=".." salaire=".." impot=".."> 以及 <erreur>...</erreur>
    • 元素 XML 的值,该事件将由 characters 过程处理。
    • XML 元素的结束,该事件将由 endElement 过程处理。
  • startElement 过程内,若处理的是 <simulation marie=".." enfants=".." salaire=".." impot=".."> 元素,则通过 attributes.getValue("属性名") 获取这四个属性。 无论哪种情况,都将元素名称存储在变量 élément 中,并将其添加到元素列表(ArrayList)中:élé1 élé2 ... élén。 该列表被管理为一个栈,其最后一个元素即为当前正在分析的 XML 元素。当触发“元素结束”事件时,列表的最后一个元素会被移除,并更新当前元素。此操作在 endElement 过程内完成。
  • characters 过程与之前示例中研究过的过程完全相同。 我们只需特别注意验证当前元素是否确实为 <erreur>,虽然在此处通常无需进行此项检查。在 startElement 过程 中也曾采取过此类预防措施,以验证所处理的元素是否为 <simulation>

6.5. 结论

得益于其返回的 XML 响应,imports 应用程序对于其设计者以及客户端应用程序的设计者而言都变得更易于管理。

  • 服务器端应用程序的设计现在可以委托给两类人员:Servlet的Java开发人员,以及负责管理服务器响应在浏览器中外观的图形设计师。后者只需了解服务器XML响应的结构,即可构建与其配套的样式表。 需要说明的是,这些样式表由独立于Java Servlet的单独XSL文件构成。因此,界面设计师可以独立于Java开发人员开展工作。
  • 客户端应用程序的设计人员同样只需了解服务器响应 XML 的结构即可。图形设计师对样式表所做的任何修改都不会影响该响应 XML,后者始终保持不变。这是一项巨大的优势。
  • 开发人员如何在不破坏现有功能的前提下升级其 Java Servlet? 首先,只要响应 XML 不变,开发者就可以随心所欲地组织 Servlet。只要保留客户端预期的 <error> <simulation> 元素,他还可以对响应 XML 进行升级。这样,他就可以向该响应中添加新的标签。 网页设计师会在样式表中考虑这些新标签,浏览器也能获取响应的新版本。而程序化的客户端则会继续使用旧模型,新标签会被直接忽略。要实现这一点,必须在对服务器响应的XML解析中,准确识别出所查找的标签。 这正是我们在税务应用程序的XML客户端中所做的:在相关流程中,我们明确指定处理<erreur>和<simulation>标签。因此,其他标签会被忽略。