用Java程序生成文本的捷径

  大多数程序都需要输出一些文本,比如邮件消息、HTML文件或控制台输出。但是,计算机本质上只能处理二进制数据,程序员必须让软件来生成可理解的文本。在这篇文章中,我要介绍的是在生成和输出文本时,为何使用模板引擎能够节省时间。你将了解模板的优点,如何针对不同的情形创建高效的模板。和System.println说再见!
  虽然程序员可以很轻松地编写出输出文字信息的代码(因为这毕竟是从Hello World范例学到的第一件事情),但通常而言,程序员不是写作或组织文字信息(如邮件)的最佳人选。因此,我们常常让市场部门或公关部门去做那些事情。但遗憾的是,即使对于最普通的邮件,编写者也常常依赖程序输出来完成任务。无论是对于邮件编写者还是程序员,这种合作方式都很容易带来误解和造成失误。
  请看一个例子:一个Java程序从某个数据源收集一些客户信息,通过email给公司的每一个客户发送帐户余额信息。下面是完成这个任务的Java程序(完整的示例程序代码可以从本文最后下载):
  for(int i=0;i{Customer customer=(Customer)customers.get(i);StringBuffer message=new StringBuffer();message.append("尊敬的先生/女士:");message.append(customer.getCustName());message.append("\n");message.append("\n");message.append("您的帐户余额是");message.append(customer.getAccountTotal());message.append("\n");message.append("\n");message.append("致礼!");message.append("\n");message.append("某某装饰品公司");//发送email mm.sendMail(customer.getFirstName(),customer.getEmail(),"Account",message.toString());}
  上面的例子可谓发送消息最差劲的方法之一。由于消息嵌入到了程序代码之中,如果没有程序员的帮助,其他人几乎不可能对消息进行编辑。同时,即使对于专业的程序员,如果他不了解代码,要进行编辑也很困难。如果你预见了这些麻烦,把代码写成下面这种形式:
  static public final String STR_HELLO="尊敬的先生/女士:";static public final String STR_MESSAGE="您的帐户余额是";static public final String STR_BEY="致礼!\n某某装饰品公司";
  如果说上述代码使得消息编辑更容易,那么这种帮助也不会很多。很难要求一个不搞程序设计的人理解static和final的含义。此外,如果要改变消息的结构,上面这种代码也不够灵活。例如,人们可能要求你在邮件消息中加入更多来自数据源的信息,这时,你就得修改构造邮件的代码,或许还要添加更多的static final String对象。
  模板简介
  从文本文件装入消息文本可以解决部分问题——但不能提供动态内容,而这对于系统来说是很重要的。你需要有一种方法把动态内容插入到预先编写好的文本消息。但是,如果使用某种文本模板引擎,它就能够帮助你完成所有复杂的工作。
  模板引擎解决了把动态内容插入文本消息的问题。使用模板引擎时,我们不再把消息直接嵌入程序,而是创建一个包含文本内容的简单文本文件,称为“文本模板”。模板引擎解析文本模板,借助一些简单的模板指令,把动态内容插入模板输出结果。
  我选择的模板引擎是Jakarta Project的Velocity,但你可以任意选择其他许多模板引擎之一。Velocity和WebMacro或许是当前功能最丰富、最受欢迎的两个引擎,而且两者都按照源代码开放协议免费提供。虽然我在本文例子中使用Velocity,你可以方便地把这些例子移植到不同的模板引擎,只需遵照目标引擎的语法即可。
  我们来看看用Velocity完成的email程序例子。要编译和运行修改后的程序,你必须下载Velocity并把它加入到classpath。如果要让email部分也能正常运行,你还需要JavaMail。
  for(int i=0;i<customers.size();i++){Customer customer=(Customer)customers.get(i);//创建一个环境,并加入所有的对象VelocityContext context=new VelocityContext();context.put("CustName",customer.getCustName());context.put("total",new Double(customer.getAccountTotal()));context.put("customer",customer);//解析模板,生成结果字符串StringWriter message=new StringWriter();template.merge(context,message);//发送email mm.sendMail(customer.getFirstName(),customer.getEmail(),"Account",message.toString());}
  首先,你应该理解上面的Java源代码。这里我们不再象第一个例子那样生成文本,上面的代码引用一个称为“Velocity模板”的文本文件,然后把结果发送给收件人。Velocity模板可以是任何文本文件,但一般它包含一些用来插入动态内容的指令。
  和VelocityContext相关的部分是上述代码中最值得注意的地方。VelocityContext提供了Java程序和Velocity文本模板之间的连接,而Velocity文本模板可以由其他人来编写。在模板中,所有加入到VelocityContext的对象都可以通过put()方法第一个参数指定的名字访问。为了解其工作过程,请看下面的模板文件:
  尊敬的先生/女士:$CustName您的帐户余额是$total致礼!某某装饰品公司
  Velocity引擎读取模板文件时,它直接输出文件中所有的文本,但以$字符开头的除外。$符号标识着一个位置,在模板的输出结果中,对象的值应该插入到$符号所指示的位置。例如,Java代码中有一个context.put("CustName",customer.getCustName())语句,当Velocity模板引擎解析并输出模板的结果时,模板中所有出现$CustName的地方都将插入客户的名字;即,被加入到VelocityContext的对象的toString()方法返回值将替代Velocity变量(模板中以$开头的变量)。
  模板引擎中最强大、使用最频繁的功能之一是它通过内建的映像(Reflection)引擎查找对象信息的能力。这个映像引擎允许用一种方便的Java“.”类似的操作符,提取任意加入到VelocityContext的对象的任何公用方法的值,或对象的任意数据成员。映像引擎还带来了另外一个改进:快速引用JavaBean的属性。使用JavaBean属性的时候,我们可以忽略get方法和括号(欲知详细信息,请参考模板引擎的说明文档)。请看下面这个模板的例子。由于在前面的Java代码示例中,我把Customer对象加入到了VelocityContext,所以我可以把模板改写成下面这种形式:
  尊敬的先生/女士:$customer.CustName您的帐户余额是$customer.AccountTotal致礼!某某装饰品公司
  除了替换变量之外,象Velocity和WebMacro这类高级引擎还能做其他许多事情。它们有用来比较和迭代的内建指令(尽管比较和迭代功能是两个模板引擎之间的共同点,但它们的语法差异很大,不能完全兼容。在选择模板引擎或者更换模板引擎时,务必注意这一点)。
  举一个例子。十二月份,你的老板想要向所有的客户发一个圣诞节问候的email。你可以把这个消息加入到模板,以后再删除它。但这样的话,你得在新年那一天上班工作,以便删除圣诞问候消息。
  一种更好的办法是指示模板何时显示圣诞问候消息。为此,你首先要把当前的月份加入到VelocityContext:
  int month=(new GregorianCalendar()).get(Calendar.MONTH);//把month值加1,因为它从0开始计算context.put("month",new Integer(month+1));
  现在,你只需在模板中进行比较:
  尊敬的先生/女士:$customer.CustName您的帐户余额是$customer.AccountTotal致礼!某某装饰品公司#if($month==12)祝您和您的家人圣诞节快乐!#end
  #if指令的作用很清楚:对一个逻辑表达式进行测试,从而决定是否在模板输出结果中包含该指令块内的内容。除了简单的等于比较之外,你还可以执行更复杂的比较,但这方面的功能都与特定的模板引擎密切关联,所以这里我不再介绍。
  迭代指令和#if指令一样简单。模板引擎支持迭代Java Collections Framework的任意实现,包括Array、List和Iterator。对于JDK 1.2或者更高版本,Java的Vector和ArrayList都实现了List接口,因此它们也适合在模板引擎的迭代指令中使用。
  假设我们现在不想输出帐户余额,而是想通过迭代遍历帐户的交易记录,输出详细的报表。Customer对象的getTransactions()方法(参见下载包中完整的示例代码)返回一个List对象,List对象包含零个或者多个Transaction对象。由于List属于Java Collections Framework的一部分,我们可以用#foreach指令迭代其内容。我们不用担心如何定型对象的类型——映像引擎会为我们完成这个任务。从下面这个例子中,我们可以看出迭代的工作过程:
  尊敬的先生/女士:$customer.CustName#foreach($transaction in$customer.Transactions)$transaction.Description$transaction.Amount#end您的帐户余额是$customer.AccountTotal致礼!某某装饰品公司
  #foreach指令的一般格式是“#foreach<item>in<list>”。#foreach指令迭代list,把list中的每个元素放入item参数,然后解析#foreach块内的内容。对于list内的每个元素,#foreach块的内容都会重复解析一次。从效果上看,它相当于告诉模板引擎说:“把list中的每一个元素依次放入item变量,每次放入一个元素,输出一次#foreach块的内容”。
  MVC设计模型
  在看下一个例子之前,请回顾一下前面我们所讨论的内容。使用模板引擎最大的好处在于,它分离了代码(或程序逻辑)和表现(输出)。由于这种分离,你可以修改程序逻辑而不必担心邮件消息本身;类似地,你(或公关部门的职员)可以在不重新编译程序的情况下,重新编写邮件消息。
  实际上,我们分离了系统的数据模式(Data Model,即提供数据的类)、控制器(Controller,即邮件程序)以及视图(View,即模板)。这种三层体系称为Model-View-Controller模型(MVC)。如果遵从MVC模型,代码分成三个截然不同的层,简化了软件开发过程中所有相关人员的工作(MVC的出现已经有一段时间,参见本文最后的“参考资源”了解更多信息)。
  结合模板引擎使用的数据模式可以是任何Java对象,最好是使用Java Collection Framework的对象。控制器只要了解模板的环境(如VelocityContext),一般这种环境都很容易使用。一些关系数据库的“对象-关系”映射工具能够和模板引擎很好地协同,简化JDBC操作;对于EJB,情形也类似。
  模板引擎与MVC中视图这一部分的关系更为密切。模板语言的功能很丰富、强大,足以处理所有必需的视图功能,同时它往往很简单,不熟悉编程的人也可以使用它。模板语言不仅使得设计者从过于复杂的编程环境中解脱出来,而且它保护了系统,避免了有意或无意带来危险的代码。例如,模板的编写者不可能编写出导致无限循环的代码,或侵占大量内存的代码。不要轻估这些安全机制的价值;大多数模板编写者不懂得编程,从长远来看,避免他们接触复杂的编程环境相当于节省了你自己的时间。
  许多模板引擎的用户相信,在采用模板引擎的方案中,控制器部分和视图部分的明确分离,再加上模板引擎固有的安全机制,使得模板引擎足以成为其他内容发布系统(比如JSP)的替代方案。因此,Java模板引擎最常见的用途是替代JSP也就不足为奇了。
  HTML处理
  由于人们总是看重模板引擎用来替换JSP的作用,有时他们会忘记模板还有更广泛的用途。到目前为止,模板引擎最常见的用途是处理HTML Web内容。但我还用模板引擎生成过SQL、email、XML甚至Java源代码。在这里我只能涉及模板的部分应用,但你可以从本文最后的参考资源找到更多的例子。
  我将在下面的HTML例子中使用前面email例子的数据模式。这个HTML页面是一个假想的企业Intranet页面,它显示出客户帐户的详细信息。本例中的控制器类是一个Java Servlet,视图部分则包含一个HTML模板。下面的代码显示了Servlet类中最主要的代码。为使这个例子更具有代表性,我从头开始手工编写这个Servlet。然而,一般情况下,模板会提供一些Servlet工具,帮助用户减轻一些编写代码的负担。
  //装入模板Template template=Velocity.getTemplate("html.vm");//创建环境VelocityContext context=new VelocityContext();context.put("customers",Customer.getCustomers());//解析模板,输出应答ServletOutputStream output=response.getOutputStream();Writer writer=new OutputStreamWriter(output);template.merge(context,writer);writer.flush();
  这个例子也没有什么令人惊异的地方。和前面的例子一样,我只是把必需的对象加入到VelocityContext,然后输出解析模板的结果。但是请注意,在前面的例子中,我只把一个Customer加入到VelocityContext,这里加入到VelocityContext的却是一组Customer对象。我可以用#foreach指令迭代访问所有的Customer对象。下面是相应的HTML模板:
  <html><body><h1>客户报告</h1>#foreach($customer in$customers)<h2>$customer.CustName<h2><table>#foreach($transaction in$customer.Transactions)<tr><td width="200">$transaction.Date</td><td width="150">$transaction.Description</td><td width="100">$transaction.Amount</td></tr>#end</tr><td></td><td></td><td><b>$customer.AccountTotal</b></td><tr></table>#end</body></html>
  如果你正在规划一个工程,这个工程的需求远远超过几个HTML模板,请考虑众多以模板为基础的框架之一。这些框架不仅为生成HTML提供了模板引擎所带来的便利,而且提供许多实用工具,比如数据库连接池和安全。两个常见的例子是Turbine和Melati,它们都和Velocity以及WebMacro兼容,都是免费且源代码开放的产品。
  性能和配置
  对于大多数程序来说,模板的速度看来已经足够快;但对于大容量的Web网站,你可能要认真地考虑一下性能问题。在性能方面,模板引擎最大的特点在于模板缓冲。在模板缓冲机制的作用下,模板不再是每次出现请求的时候从磁盘读取,而是以最理想的方式在内存中保存和解析。在开发期间,模板缓冲通常处于禁用状态,因为这时请求数量较少,而且要求对页面的修改立即产生效果。部署完毕之后,模板一般不再改变,性能就成了优先考虑的问题。因此,这时你应该启用模板缓冲功能。
  对于大多数模板引擎,你可以通过应用一个设置选项或编辑Java属性文件方便地启用模板缓冲功能。在Velocity中,你可以通过Properties对象初始化模板。至于Properties对象的创建方法,你既可以手工创建,就象我前面所做的那样;或者也可以从属性文件装入。在实际应用中,后者也许是较为理想的方法。
  Properties props=new Properties();props.setProperty("file.resource.loader.cache","true");props.setProperty("file.resource.loader.modificationCheckInterval","3600");Velocity.init(props);
  通过file.resource.loader.cache属性可以把缓冲设置成true或false,而file.resource.loader.modificationCheckInterval属性设置的是检查文件是否改变的间隔秒数。在这里我无法详细介绍所有的属性,请参考模板引擎的文档了解更多信息。
  ■结束语
  免费的高级模板引擎使我们能够把模板功能加入到几乎所有的Java应用。这些模板引擎为程序员提供了易用的工具,为模板编写者提供了简单的模板语言,使得开发者更有信心编写出高质量的代码。
  模板分离了程序代码和应用的表现部分,极大地方便了程序员和内容制作者的工作。模板把程序员从混合了大量文本信息的杂乱代码中解放出来;使得制作文本内容的人无需面对程序逻辑,就可以轻松地编写和修改内容。
  模板清楚地分离了程序逻辑和文本表现代码,从而也为设计更好的MVC系统提供了方便。因此,模板为替换其他内容发布系统(比如JSP)提供了一种有吸引力的方案,因为它能够在不增加复杂性的情况下,改进应用的整体设计。
  ■参考资源
  下载本文示例的完整代码
  其他模板应用的例子
  Sun的JavaMail
  Velocity和WebMacro是两个最受欢迎的模板引擎:
  Velocity
  WebMacro
  了解更多有关MVC的知识,看看它能够为你的程序设计带来什么帮助
  基于模板的Web应用框架:
  Turbine
  The Melati project
  对象-关系工具:
  The ExoLab Group
  Osage