我对 Velocity Hibernate Tools 中使用的模板中忽略甚至隐藏错误和异常的能力感到不满。

这篇博客讲述了为什么以及如何 FreeMarker 成为我的新兴趣。如果你只想看结果,请前往 TOOLS_FREEMARKER 分支获取代码...继续阅读以获取完整故事。

Velocity 的问题

我开始看到越来越多的论坛帖子和技术问题报告,这些问题是由用户模板中的拼写错误或更糟糕的是 Hibernate Tools 中的错误引起的。如果 Velocity 实际上能够告诉错误发生在模板的哪个位置,许多这些问题都可以在几秒钟内解决;如果暴露了底层异常,单元测试也会失败;但 Velocity 没有做到。

我已经为 Velocity 错误处理添加了我能想到的所有安全预防措施。我创建了自己的 UberSpect 和 EventHandler 实现,不允许调用不存在的方法,并调整了日志设置以使其更具信息性;但它(几乎)没有解决所有可能出现的问题。

即使在 WARN 和 INFO 级别,Velocity 的日志记录也过于频繁,其中一个可能的原因是开发人员知道 Velocity 忽略了它应该失败的情况,因此由于在 Velocity 中没有其他简单的方法实现,他们将它们放入日志中,希望用户意外发现!

最初的选择是 Velocity,因为它是当时最大的玩家,我天真地认为,由于这么多人使用它,如果存在问题,它很快就会被修复。

随着时间的推移,我了解到情况绝对不是这样。

FreeMarker 的优点

上周我决定寻找替代方案,我找到的唯一真正的替代方案是FreeMarker;其他所有方案要么太简单,要么过于复杂,不适合Hibernate Tools的需求。现在,我已经花费了1.5天将现有的Velocity模板转换为FreeMarker,我非常高兴自己做出了这个决定。

以下是FreeMarker之美的几个例子

假设我们有一个以下这样的bean

public class Table {
 String getName();
}

该bean可以通过以下代码中的table获取

${table.namee}

在Velocity中,这个错误会被默认忽略,通过自定义EventHandler可以说服它抛出异常,异常显示如下

Caused by: java.lang.IllegalArgumentException: $table.namee is not a valid reference.
at org.hibernate.tool.hbm2x.HibernateEventHandler.referenceInsert([=>HibernateEventHandler.java:11])
        at org.apache.velocity.app.event.EventCartridge.referenceInsert([=>EventCartridge.java:131])
        ... 19 more

没有关于哪个模板以及模板中出错位置的信息。

在FreeMarker中,我得到了以下结果,无需特殊配置和自定义代码

Expression table.namee is undefined on line 15, column 14 in doc/tables/table.ftl.
The problematic instruction:
----------
==> ${table.namee} [on line 15, column 12 in doc/tables/table.ftl]
----------

Java backtrace for programmers:
----------
freemarker.core.InvalidReferenceException: 
 Expression table.namee is undefined on line 15, column 14 in doc/tables/table.ftl.
    at freemarker.core.TemplateObject.assertNonNull([=>TemplateObject.java:124])
    at freemarker.core.Expression.getStringValue([=>Expression.java:118])
    at freemarker.core.Expression.getStringValue([=>Expression.java:93])
    ...

太棒了!而且更好,第15行,...在例如Eclipse Console视图中就像一个链接。点击它将带你到table.ftl文件中错误的位置。

如果你引用了不存在的函数,你也会得到类似且精确的错误信息。太棒了!最好的是,如果我真的想让FreeMarker忽略这个错误,我可以通过安装不同的异常处理器来实现。但这是我的选择,不是难以更改的行为。

FreeMarker的内置原语也非常出色,例如<#assign>,它允许我将任何生成的输出存储在变量中以供以后使用。

${pojo.getPackageDeclaration()}
// Generated ${date} by Hibernate Tools ${version}
<#assign classbody>
 <#include "PojoTypeDeclaration.ftl"/> {
  ..more template code..
 }
</#assign>

${pojo.generateImports()}
${classbody}

这允许我去除需要使用Velocity的神奇第二次遍历的需求。在优秀的FreeMarker 文档中还有更多这样的宝石。

FreeMarker的另一个优点是配置API。让我们比较一下,以下是我们的Velocity设置

engine = new VelocityEngine();
context = new VelocityContext();

EventCartridge ec = new EventCartridge();
ec.addEventHandler(new HibernateEventHandler()); // stricter evaluation
ec.attachToContext( context );
            
Properties p = new Properties();
p.setProperty(RuntimeConstants.RUNTIME_LOG_LOGSYSTEM_CLASS, 
              "org.apache.velocity.tools.generic.log.CommonsLogLogSystem");
p.setProperty(CommonsLogLogSystem.LOGSYSTEM_COMMONS_LOG_NAME, 
              "org.hibernate.tool.hbm2x.template");

p.setProperty( RuntimeConstants.UBERSPECT_CLASSNAME, HibernateUberspect.class.getName() ); // stricter evaluation

p.setProperty("velocimacro.library",""); // make it shut up about VM_global_library blah
      
p.setProperty("resource.loader", "file, class");
p.setProperty("file.resource.loader.path", directory );
p.setProperty("class.resource.loader.class", ClasspathResourceLoader.class.getName() );

engine.init(p);

以下是相应的FreeMarker配置

engine = new Configuration();
context = new SimpleHash(ObjectWrapper.BEANS_WRAPPER);

//Logger.setCategoryPrefix("org.hibernate.tool.hbm2x.template"); 
// Not really needed since the logging is much more sensible.

freeMarkerEngine.setTemplateLoader(new MultiTemplateLoader( 
                                    new FileTemplateLoader(directory), 
                                    new ClassTemplateLoader(this.getClass(),"/"));

注意区别?FreeMarker有良好的默认实践,并实际上允许我使用Java代码来配置它;这是一个很酷的概念。

我在FreeMarker中发现的两件“不好”的事情是它的语法基于<#..>在尝试在XML编辑器中显示时效果不佳。这个“问题”在最新版本中被“解决”,因为它也允许[#...]语法。

另一个更大的问题是${}#{}是不可逃逸的。这种语法在生成ant构建和jsp文件的模板中会冲突。

在Velocity中,它们只是被忽略(唯一有用的是忽略它们的地方)。FreeMarker会抱怨因为这些值未定义。不幸的是,没有简单易行的方法来转义这些字符。以下显示了我找到的允许我输出${..}的方法

${r"${build.dir}"}  
${'$í}{build.dir} 
<#noescape>${build.dir}</noescape>

尽管如此,出色的异常处理、强大的模板语言和配置API使得FreeMarker成为Hibernate Tools的一个更好的选择。

那么现在呢?

Velocity为我服务得很好,可能为许多项目服务得很好;但它并不适合Hibernate Tools。今天,我相信,如果我从一开始就决定使用FreeMarker,我就能为自己和Hibernate世界节省很多麻烦。

请前往TOOLS_FREEMARKER分支看看。那里的代码将在不久的将来合并到主开发中,除非有人提出非常好的理由不这样做;)

说公平点,我必须告诉你,Velocity 1.5目前正在开发中,它似乎解决了这些问题中的某些问题,但并不是完全解决,并且Velocity有一些外部依赖,我不想添加到工具项目中。


返回顶部