这是一个Java实现的Mustache模板语言。
动机
-
零依赖。你可以在项目中只包含这一个小型库就可以开始使用模板。
-
可用于多种目标平台。该实现对JVM的要求非常有限,因此可以在Android或其他受限JVM上使用。甚至可以避免使用反射,而将所有数据作为一系列嵌套映射提供。
-
对Proguard和JarJar友好。虽然库会反射性地访问你的数据(如果你需要),但库内部不会使用其他反射或按名称实例化类。因此,你可以使用Proguard或JarJar嵌入它而不会遇到任何烦人的意外。
-
最小API占用。你真正只需要了解两个方法:
compile
和execute
。在性能不重要的情况下,你甚至可以将它们链接在一起。
在上述动机的基础上,该实现还努力提供额外的好处:
- 它可通过Maven Central获得,详情见下文。
- 它具有相当不错的性能。模板的解析与执行是分开的。模板会根据(上下文类,名称)对来专门化其变量,这样如果变量首次解析为(例如)上下文对象的字段,那么在后续的模板调用中会直接尝试这种方式,只有在访问变量作为字段失败时才会尝试较慢的完整解析。
获取
JMustache可通过Maven Central获得,因此可以通过添加依赖com.samskivert:jmustache:1.15
轻松添加到你的Maven、Ivy等项目中。或者下载预构建的jar文件。
文档
除了下面的使用部分,以下文档可能有用:
使用
使用JMustache非常简单。将你的模板作为String
或Reader
提供,就可以得到一个可以在任何上下文中执行的Template
:
String text = "一、二、{{three}}。三,先生!";
Template tmpl = Mustache.compiler().compile(text);
Map<String, String> data = new HashMap<String, String>();
data.put("three", "五");
System.out.println(tmpl.execute(data));
// 结果: "一、二、五。三,先生!"
如果你在做更严肃的事情,可以使用Reader
和Writer
:
void executeTemplate (Reader template, Writer out, Map<String, String> data) {
Mustache.compiler().compile(template).execute(data, out);
}
执行上下文可以是任何Java对象。变量将通过以下机制解析:
- 如果上下文是
MustacheCustomContext
,将使用MustacheCustomContext.get
。 - 如果上下文是
Map
,将使用Map.get
。 - 如果存在与变量同名的非void方法,将调用该方法。
- 如果存在名为(对于变量
foo
)getFoo
的非void方法,将调用该方法。 - 如果存在与变量同名的字段,将使用其内容。
示例:
class Person {
public final String name;
public Person (String name, int age) {
this.name = name;
_age = age;
}
public int getAge () {
return _age;
}
protected int _age;
}
String tmpl = "{{#persons}}{{name}}: {{age}}\n{{/persons}}";
Mustache.compiler().compile(tmpl).execute(new Object() {
Object persons = Arrays.asList(new Person("Elvis", 75), new Person("Madonna", 52));
});
// 结果:
// Elvis: 75
// Madonna: 52
如你所见,字段(和方法)无需是public。在创建的匿名类中作为上下文的persons
字段是可访问的。注意,在沙箱安全环境中,使用非公开字段将不起作用。
区段的行为如你所期望的:
Boolean
值启用或禁用该区段。- 数组、
Iterator
或Iterable
值会重复执行该区段,每次迭代使用每个元素作为上下文。空集合会导致该区段在模板中被包含零次。 - 不可解析或null值被视为false。可以通过使用
strictSections()
来更改此行为。详见_默认值_。 - 任何其他对象都会导致该区段执行一次,以该对象作为上下文。
请参阅MustacheTest.java中的代码以获取具体示例。另请参阅Mustache文档以了解模板语法的详细信息。
局部模板
如果你想使用局部模板(例如{{>subtmpl}}
),在创建编译器时必须提供一个Mustache.TemplateLoader
。例如:
final File templateDir = ...;
Mustache.Compiler c = Mustache.compiler().withLoader(new Mustache.TemplateLoader() {
public Reader getTemplate (String name) {
return new FileReader(new File(templateDir, name));
}
});
String tmpl = "...{{>subtmpl}}...";
c.compile(tmpl).execute();
上面的代码片段在编译模板时将加载new File(templateDir, "subtmpl")
。
Lambda表达式
JMustache通过向你传递一个Template.Fragment
实例来实现lambda表达式,你可以用它来执行传递给lambda的模板片段。你可以装饰片段执行的结果,如标准Mustache文档中关于lambda的示例所示:
String tmpl = "{{#bold}}{{name}}真棒。{{/bold}}";
Mustache.compiler().compile(tmpl).execute(new Object() {
String name = "威利";
Mustache.Lambda bold = new Mustache.Lambda() {
public void execute (Template.Fragment frag, Writer out) throws IOException {
out.write("<b>");
frag.execute(out);
out.write("</b>");
}
};
});
// 结果:
<b>威利真棒。</b>
你还可以获取片段执行的结果来进行国际化或缓存等操作:
Object ctx = new Object() {
Mustache.Lambda i18n = new Mustache.Lambda() {
public void execute (Template.Fragment frag, Writer out) throws IOException {
String key = frag.execute();
String text = // 在i18n系统中查找key
out.write(text);
}
};
};
// 模板可能看起来像这样:
<h2>{{#i18n}}title{{/i18n}}</h2>
{{#i18n}}welcome_msg{{/i18n}}
还有对反编译(取消执行)模板并获取区段中包含的原始Mustache模板文本的有限支持。有关限制的详细信息,请参阅Template.Fragment的文档。
默认值
默认情况下,每当无法解析变量或变量解析为null时(区段除外,见下文),都会抛出异常。你可以通过两种方式更改此行为。如果你想在所有此类情况下提供一个值,请使用defaultValue()
:
String tmpl = "{{exists}} {{nullValued}} {{doesNotExist}}?";
Mustache.compiler().defaultValue("什么").compile(tmpl).execute(new Object() {
String exists = "说";
String nullValued = null;
// String doesNotExist
});
// 结果:
说 什么 什么?
如果你只想为解析为null的变量提供默认值,并希望在无法解析变量的情况下保留异常,请使用nullValue()
:
String tmpl = "{{exists}} {{nullValued}} {{doesNotExist}}?";
Mustache.compiler().nullValue("什么").compile(tmpl).execute(new Object() {
String exists = "说";
String nullValued = null;
// String doesNotExist
});
// 执行模板时会抛出MustacheException,因为无法解析doesNotExist
当使用Map
作为上下文时,nullValue()
仅在map包含映射到null
的情况下使用。如果map缺少给定变量的映射,则认为它不可解析并抛出异常。
Map<String,String> map = new HashMap<String,String>();
map.put("exists", "说");
map.put("nullValued", null);
// 没有"doesNotExist"的映射
String tmpl = "{{exists}} {{nullValued}} {{doesNotExist}}?";
Mustache.compiler().nullValue("什么").compile(tmpl).execute(map);
// 执行模板时会抛出MustacheException,因为无法解析doesNotExist
不要在编译器配置中同时使用defaultValue
和nullValue
。每个都会覆盖另一个,所以无论你最后调用哪个,都会得到那个行为。即使你意外地做对了,代码也会很混乱,所以不要同时调用两者,使用其中一个。
区段
区段不受nullValue()
或defaultValue()
设置的影响。它们的行为由单独的配置strictSections()
控制。
默认情况下,不可解析或解析为null
的区段将被省略(相反,不可解析或解析为null
的反向区段将被包含)。如果你使用strictSections(true)
,引用不可解析值的区段将始终抛出异常。无论strictSections()
设置如何,引用可解析但为null
值的区段永远不会抛出异常。
扩展
JMustache用一些额外的功能扩展了基本的Mustache模板语言。这些附加功能列举如下:
默认不转义HTML
你可以在获取编译器时更改默认的HTML转义行为:
Mustache.compiler().escapeHTML(false).compile("{{foo}}").execute(new Object() {
String foo = "<bar>";
});
// 结果: <bar>
// 而不是: <bar>
用户定义的对象格式化
默认情况下,JMustache使用String.valueOf
在渲染模板时将对象转换为字符串。你可以通过实现Mustache.Formatter
接口来自定义此格式化:
Mustache.compiler().withFormatter(new Mustache.Formatter() {
public String format (Object value) {
if (value instanceof Date) return _fmt.format((Date)value);
else return String.valueOf(value);
}
protected DateFormat _fmt = new SimpleDateFormat("yyyy/MM/dd");
}).compile("{{msg}}: {{today}}").execute(new Object() {
String msg = "日期";
Date today = new Date();
})
// 结果: 日期: 2013/01/08
用户定义的转义规则
你可以在获取编译器时更改转义行为,以支持HTML和纯文本以外的文件格式。
如果你只需要替换文本中的固定字符串,可以使用Escapers.simple
:
String[][] escapes = {{ "[", "[[" }, { "]", "]]" }};
Mustache.compiler().withEscaper(Escapers.simple(escapes)).
compile("{{foo}}").execute(new Object() {
String foo = "[bar]";
});
// 结果: [[bar]]
或者你可以直接实现Mustache.Escaper
接口以更好地控制转义过程。
特殊变量
this
你可以使用特殊变量this
来引用上下文对象本身,而不是它的某个成员。这在迭代列表时特别有用。
Mustache.compiler().compile("{{this}}").execute("你好"); // 返回: 你好
Mustache.compiler().compile("{{#names}}{{this}}{{/names}}").execute(new Object() {
List<String> names () { return Arrays.asList("汤姆", "迪克", "哈利"); }
});
// 结果: 汤姆迪克哈利
请注意,你也可以使用特殊变量.
来表示相同的意思。
Mustache.compiler().compile("{{.}}").execute("你好"); // 返回: 你好
Mustache.compiler().compile("{{#names}}{{.}}{{/names}}").execute(new Object() {
List<String> names () { return Arrays.asList("汤姆", "迪克", "哈利"); }
});
// 结果: 汤姆迪克哈利
.
显然受其他Mustache实现支持,尽管它没有出现在官方文档中。
-first和-last
你可以使用特殊变量-first
和-last
对列表元素进行特殊处理。当在处理列表元素中的第一个元素的区段内时,-first
解析为true
。在所有其他时候,它解析为false
。当在处理列表元素中的最后一个元素的区段内时,`-last
Mustache.compiler().compile("你好 {{field.who}}!").execute(new Object() {
public Object field = new Object() {
public String who () { return "世界"; }
}
});
// 结果: 你好 世界!
通过利用反射和Bean属性风格的查找,你可以做一些奇怪的事情:
Mustache.compiler().compile("你好 {{class.name}}!").execute(new Object());
// 结果: 你好 java.lang.Object!
注意,复合变量本质上是使用单一部分的简写形式。上面的例子也可以表示为:
你好 {{#field}}{{who}}{{/field}}!
你好 {{#class}}{{name}}{{/class}}!
还要注意,嵌套的单一部分和复合变量之间存在一个语义差异:在解析复合变量的第一个组件的对象后,在解析子组件时不会搜索父上下文。
换行符修剪
如果开始或结束部分标签是一行中唯一的内容,则会修剪标签周围的任何空白和后面的行终止符。这允许文明的模板,比如:
最喜欢的食物:
<ul>
{{#people}}
<li>{{first_name}} {{last_name}} 喜欢 {{favorite_food}}。</li>
{{/people}}
</ul>
它会产生如下输出:
最喜欢的食物:
<ul>
<li>埃尔维斯 普雷斯利 喜欢 花生酱。</li>
<li>圣雄 甘地 喜欢 阿卢杜姆。</li>
</ul>
而不是:
最喜欢的食物:
<ul>
<li>埃尔维斯 普雷斯利 喜欢 花生酱。</li>
<li>圣雄 甘地 喜欢 阿卢杜姆。</li>
</ul>
如果没有换行符修剪,就会产生这样的结果。
嵌套上下文
如果在嵌套上下文中找不到变量,它会在外层上下文中解析。这允许以下用法:
String template = "{{outer}}:\n{{#inner}}{{outer}}.{{this}}\n{{/inner}}";
Mustache.compiler().compile(template).execute(new Object() {
String outer = "foo";
List<String> inner = Arrays.asList("bar", "baz", "bif");
});
// 结果:
// foo:
// foo.bar
// foo.baz
// foo.bif
注意,如果变量在内部上下文中定义,它会覆盖外部上下文中的同名变量。目前没有办法访问外部上下文中的变量。
可反转的Lambda
对于某些应用程序,Lambda在反向部分执行而不是完全省略该部分可能很有用。这允许在将模板静态转换为其他语言或上下文时进行适当的条件替换:
String template = "{{#condition}}结果为真{{/condition}}\n" +
"{{^condition}}结果为假{{/condition}}";
Mustache.compiler().compile(template).execute(new Object() {
Mustache.InvertibleLambda condition = new Mustache.InvertibleLambda() {
public void execute (Template.Fragment frag, Writer out) throws IOException {
// 当Lambda在正常部分中被引用时执行此方法
out.write("if (condition) {console.log(\"");
out.write(toJavaScriptLiteral(frag.execute()));
out.write("\")}");
}
public void executeInverse (Template.Fragment frag, Writer out) throws IOException {
// 当Lambda在反向部分中被引用时执行此方法
out.write("if (!condition) {console.log(\"");
out.write(toJavaScriptLiteral(frag.execute()));
out.write("\")}");
}
private String toJavaScriptLiteral (String execute) {
// 注意:这不是JavaScript字符串字面量转义的完整实现
return execute.replaceAll("\\\\", "\\\\\\\\").replaceAll("\"", "\\\\\"");
}
};
});
// 结果:
// if (condition) {console.log("结果为真")}
// if (!condition) {console.log("结果为假")}
当然,你不仅限于条件替换 - 只要需要具有两种操作模式的单个函数,就可以使用InvertibleLambda。
标准模式
在创建编译器时,可以禁用这些扩展中更具侵入性的部分,特别是搜索父上下文和使用复合变量,如下所示:
Map<String,String> ctx = new HashMap<String,String>();
ctx.put("foo.bar", "baz");
Mustache.compiler().standardsMode(true).compile("{{foo.bar}}").execute(ctx);
// 结果: baz
线程安全
JMustache在内部是线程安全的,但有以下注意事项:
-
编译:编译模板会调用各种辅助类:
Mustache.Formatter
,Mustache.Escaper
,Mustache.TemplateLoader
,Mustache.Collector
。这些类的默认实现是线程安全的,但如果你提供自定义实例,则必须确保你的自定义实例是线程安全的。 -
执行:执行模板可能会调用一些辅助类:
Mustache.Lambda
,Mustache.VariableFetcher
。这些类的默认实现是线程安全的,但如果你提供自定义实例,则必须确保你的自定义实例是线程安全的。 -
上下文数据:如果在执行模板时修改传递给模板执行的上下文数据,则会引入竞态条件。理论上可以使用线程安全的映射(
ConcurrentHashMap
或Collections.synchronizedMap
)作为上下文数据,这将允许你在基于该数据渲染模板的同时修改数据,但这样做是在玩火。我不建议这样做。如果你的数据是作为POJO提供的,其中字段或方法通过反射被调用以填充模板,那么volatile字段和同步方法同样可以用来支持同时读取和修改,但是你很容易犯错误引入竞态条件或在执行模板时导致奇怪的行为。当通过同时运行的线程渲染相同的模板时,最安全的方法是将不可变/不变的数据作为每次执行的上下文传递。 -
VariableFetcher
缓存:模板执行使用一个内部缓存来存储已解析的VariableFetcher
实例(因为解析变量获取器是昂贵的)。这个缓存通过使用ConcurrentHashMap
来实现线程安全。如果两个线程同时解析同一个变量,可能会做一些额外的工作,但它们不会相互冲突,它们只会都解析该变量,而不是一个解析变量而另一个使用缓存的解析结果。
所以总结是:只要你提供的所有辅助类都是线程安全的(或者你使用默认值),在线程之间共享一个Mustache.Compiler
实例来编译模板是安全的。如果你在执行时向模板传递不可变数据,那么让多个线程同时执行单个Template
实例是安全的。
限制
为了简单起见,省略或简化了Mustache的一些功能:
{{= =}}
只支持一个或两个字符的分隔符。这只是因为我很懒,它简化了解析器。