一、前言
常见的Java模板引擎有JSP、Freemark,Velocity。在MVC三层框架中,模板引擎属于view层,实质是把model层内容展现到前台页面的一个引擎,velocity以其前后端解耦使前后台可以同时开发和其语法的简易性得到了广泛的应用,集团WebX框架就建议使用它作为模板引擎。
二、原理
2.1 架构介绍
打开velocity的源码包,从代码结构看velocity主要包括app、context、runtime、event、texen和一些util类
1)、app模块
源码org.apache.velocity.app下面主要有两个类Velocity和VelocityEngine。
- Velocity ,主要对外提供一些static方法,可以通过类名直接调用,只要通过Velocity创建一个模块,在创建一个存放变量的context,就可以渲染,如下:
另外Velocity功能是委托给RuntimeInstance来具体实现的,并且维护的是一个单件实例,就是说在同一个jvm中,只有一个Velocity的实例,这给资源共享和配置本地化带来的方便,这为在通一个JVM的不同应用见共享模块提供了方便。
- VelocityEngine ,相比于Velocity提供了更加强大的功能,框架开发者一般使用这个类在框架中使用velocity模板渲染功能,内部也是是委托给RuntimeInstance来具体实现的,但是每个VelocityEngine都有一个自己的RuntimeInstance实例。也就是说在一个JVM中可以有多个VelocityEngine实例,每个实例都可以定制化自己的配置,这为在同一个应用中配置不同的模板路径和logger提供了方便。
例如springmvc中初始化一个veloctiy引擎方式如下:
2)、Context模块
源码org.apache.velocity.context包下的Context,AbstractContext,还有org.apache.velocity下的VelocityContext。主要功能是提供对模板渲染所需要的变量的封装管理.
Context设计目的:
- 作为一个适配器,便于与其他框架集成
例如SpringMVC传递参数的是一个Map的数据结构,那么如果springmvc中使用velocity则需要把map里面存放的变量适配到context中,这个是直接把map作为VelocityContext构造函数参数适配的。但是webx使用的是自己的context,PullableMappedContext存放变量,那么就需要继承velocity的AbstractContext实现一个适配器TemplateContextAdapter来把自己的context转换为velocity所需要的context.
- Velocity内部数据隔离,Velocity不同模块通过传递参数方式进行处理,利于模块之间的解耦。
3)、RunTime模块
源码org.apache.velocity.runtime包下:
负责加载模板文件,解析为JavaCC语法树,使用深度遍历算法渲染语法书节点,生成渲染结果。
4)、RuntimeInstance
负责解析模板文件为AST结构,velocity和velocityengine内部都是委托给它来实现功能。
5)、util模块
一些工具类,例如SimplePool是一个对象池,里面默认缓存20个Parser。CalssUtiles是一个简单的从classloader操作类和资源的函数类。
2.2 源码分析
2.2.1 试验准备
pom中添加velocity依赖
<dependency>
<groupId>velocity-tools</groupId>
<artifactId>velocity-tools-generic</artifactId>
<version>1.4</version>
</dependency>
测试java代码:
public static void main(String[] args) {try { // 初始化(1)
Velocity.init("velocity.properties"); // 创建context,存放变量(2)
VelocityContext context = new VelocityContext();
Person person = new Person();
person.setName("jiaduo");
context.put("person", person); // 加载模板文件到内存(3)
Template template = null;
String templateFile = "healthview.vm";
template = Velocity.getTemplate(templateFile); // 渲染(4)
StringWriter stringWriter = new StringWriter();
template.merge(context, stringWriter); // 打印结果
System.out.println(stringWriter.toString());
} catch (Exception e) {
e.printStackTrace();
}
}
healthview.vm内容:
<html>
<div>$!{person.sayHello()}:$!{person.name}</div>
</html>
velocity.properties内容:
file.resource.loader.path = /Users/zhuizhumengxiang/workspace/mytool/SpringLean/src/
2.2.2 源码分析
先看下(1)Velocity.init()时序图:
从时序图可知Velocity是委托给RuntimeInstance去实现初始化工作,RuntimeSingleton则是保证RuntimeInstance的单例。init里面首先解析用户传递的配置文件,然后解析:
最后使用用户配置文件配置项覆盖默认配置项。
public synchronized void init()
{ if (!initialized && !initializing)
{
log.debug("Initializing Velocity, Calling init()...");
initializing = true;
log.trace("*******************************************************************");
log.debug("Starting Apache Velocity v1.7 (compiled: 2010-11-19 12:14:37)");
log.trace("RuntimeInstance initializing.");
initializeProperties();//配置文件解析
initializeLog();//初始化日志
initializeResourceManager();//初始化资源管理器和资源加载器
initializeDirectives();//初始化Directives
initializeEventHandlers();// 初始化事件处理器
initializeParserPool();//初始化解析器 对象池
initializeIntrospection();// 初始化自省
initializeEvaluateScopeSettings(); /*
* initialize the VM Factory. It will use the properties
* accessable from Runtime, so keep this here at the end.
*/
vmFactory.initVelocimacro();
log.trace("RuntimeInstance successfully initialized.");
initialized = true;
initializing = false;
}
}
initializeResourceManager的代码逻辑:
private void initializeResourceManager()
{ /*
* org.apache.velocity.runtime.resource.ResourceManagerImpl
*/
String rm = getString(RuntimeConstants.RESOURCE_MANAGER_CLASS); if (rm != null && rm.length() > 0)
{
Object o = null; //创建资源管理器实例
try
{
o = ClassUtils.getNewInstance( rm );
}
...
resourceManager = (ResourceManager) o; //初始化资源管理器
resourceManager.initialize(this);
}
...
}//初始化资源管理器public synchronized void initialize(final RuntimeServices rsvc){ ...
ResourceLoader resourceLoader = null; this.rsvc = rsvc;
log = rsvc.getLog();
log.trace("Default ResourceManager initializing. (" + this.getClass() + ")");
assembleResourceLoaderInitializers(); //创建资源加载器
for (Iterator it = sourceInitializerList.iterator(); it.hasNext();)
{ /**
* Resource loader can be loaded either via class name or be passed
* in as an instance.
*/
ExtendedProperties configuration = (ExtendedProperties) it.next();
String loaderClass = StringUtils.nullTrim(configuration.getString("class"));
ResourceLoader loaderInstance = (ResourceLoader) configuration.get("instance"); if (loaderInstance != null)
{
resourceLoader = loaderInstance;
} else if (loaderClass != null)
{
resourceLoader = ResourceLoaderFactory.getLoader(rsvc, loaderClass);
}
...
resourceLoader.commonInit(rsvc, configuration);
resourceLoader.init(configuration);
resourceLoaders.add(resourceLoader);
} //org.apache.velocity.runtime.resource.ResourceCacheImpl
String cacheClassName = rsvc.getString(RuntimeConstants.RESOURCE_MANAGER_CACHE_CLASS);
Object cacheObject = null; //创建缓存实例
if (org.apache.commons.lang.StringUtils.isNotEmpty(cacheClassName))
{ try
{
cacheObject = ClassUtils.getNewInstance(cacheClassName);
}
...
} /*
* if we didn't get through that, just use the default.
*/
if (cacheObject == null)
{
cacheObject = new ResourceCacheImpl();
}
globalCache = (ResourceCache) cacheObject; //初始化缓存
globalCache.initialize(rsvc);
} //初始化缓存
public void initialize( RuntimeServices rs ){
rsvc = rs; //默认配置文件里没这个变量,所以默认最多缓存89个模板文件
int maxSize =
rsvc.getInt(RuntimeConstants.RESOURCE_MANAGER_DEFAULTCACHE_SIZE, 89); if (maxSize > 0)
{ // Create a whole new Map here to avoid hanging on to a
// handle to the unsynch'd LRUMap for our lifetime.
Map lruCache = Collections.synchronizedMap(new LRUMap(maxSize));
lruCache.putAll(cache);
cache = lruCache;
}
rsvc.getLog().debug("ResourceCache: initialized ("+this.getClass()+") with "+
cache.getClass()+" cache map.");
}
initializeParserPool逻辑,目的应该是为了提高性能:
private void initializeParserPool()
{ /*
* 配置中获取,org.apache.velocity.runtime.ParserPoolImpl
*/
String pp = getString(RuntimeConstants.PARSER_POOL_CLASS); if (pp != null && pp.length() > 0)
{
Object o = null; try
{//实例化
o = ClassUtils.getNewInstance( pp );
}
...
parserPool = (ParserPool) o; //调用初始化方法,创建parser对象池
parserPool.initialize(this);
}
...
}//创建Parser对象池
public void initialize(RuntimeServices rsvc)
{ //默认为20个
max = rsvc.getInt(RuntimeConstants.PARSER_POOL_SIZE, RuntimeConstants.NUMBER_OF_PARSERS);
pool = new SimplePool(max); for (int i = 0; i < max; i++)
{
pool.put(rsvc.createNewParser());
} if (rsvc.getLog().isDebugEnabled())
{
rsvc.getLog().debug("Created '" + max + "' parsers.");
}
}
initializeIntrospection的逻辑:
private void initializeIntrospection()
{//[org.apache.velocity.util.introspection.UberspectImpl]
String[] uberspectors = configuration.getStringArray(RuntimeConstants.UBERSPECT_CLASSNAME); for (int i=0; i <uberspectors.length;i++)
{
String rm = uberspectors[i];
Object o = null; try
{
o = ClassUtils.getNewInstance( rm );
}
。。。
Uberspect u = (Uberspect)o;
{ if (u instanceof ChainableUberspector)
{
((ChainableUberspector)u).wrap(uberSpect);
uberSpect = u;
} else
{
uberSpect = new LinkingUberspector(uberSpect,u);
}
}
} if(uberSpect != null)
{
uberSpect.init();
}
...
} public void init()
{
introspector = new Introspector(log);
}
在看下(2)代码如下:
public Object put(String key, Object value)
{ if (key == null)
{ return null;
} return internalPut(key.intern(), value);
} public Object internalPut( String key, Object value )
{ //context是一个HashMap
return context.put( key, value );
}
在看下(3)时序图
从时序图知道首先去加载模板文件到内存,代码如下:
public Resource getResource(final String resourceName, final int resourceType, final String encoding)
throws ResourceNotFoundException,
ParseErrorException { //先从缓存里面查找
String resourceKey = resourceType + resourceName;
Resource resource = globalCache.get(resourceKey); if (resource != null)
{ try
{ // 缓存命中,则看是否开定时从磁盘加载,定时到了则从磁盘加载
if (resource.requiresChecking())
{ //从磁盘加载
resource = refreshResource(resource, encoding);
}
}
...
} else
{ try
{ //从磁盘加载
resource = loadResource(resourceName, resourceType, encoding); //开启了缓存,则放入缓存
if (resource.getResourceLoader().isCachingOn())
{
globalCache.put(resourceKey, resource);
}
}
...
} //返回资源
return resource;
}
file.resource.loader.cache = false
file.resource.loader.modificationCheckInterval = 2
默认不开启缓存,CheckInterval = 2。
然后解析模板文件为ast结构
loadResource->()
{
resource.process()
{
RuntimeInstance.parse();//解析模板文件为AST node结构
}
}public SimpleNode parse(Reader reader, String templateName, boolean dumpNamespace)
throws ParseException {
requireInitialization();
Parser parser = (Parser) parserPool.get(); boolean keepParser = true; if (parser == null)
{ //没有可用的则创建
if (log.isInfoEnabled())
{
log.info("Runtime : ran out of parsers. Creating a new one. "
+ " Please increment the parser.pool.size property."
+ " The current value is too small.");
}
parser = createNewParser();
keepParser = false;
} try
{
... return parser.parse(reader, templateName);
} finally
{ //如果从对象池获取则使用后归还
if (keepParser)
{
parserPool.put(parser);
}
}
}
目前template里面的data对应内容:
再看下(4)时序图为:
如图debug可知velocity把healthview.vm解析为了5段:
画出ast树图如下:
其中从左向右第一个节点是vm中 <html> <div>
解析为ASTText文本节点内容为:[<html> <div>]
第二个节点是对$!{person.sayHello()}
的解析,是一个ASTReference节点,该节点有一个子节点ASTmethod,第三个节点是对vm中:
解析为ASTText文本节点内容为:[ :]
第四个节点是对vm中$!{person.name}
的解析,是是一个ASTReference节点,该节点子节点是ASTIdentifier第五个节点是VM中</div></html>
的解析,解析为ASTText文本节点内容为:[</div></html>]]
ASTProcess的render方法是采用树的深度遍历算法来渲染节点的,具体代码:
public boolean render( InternalContextAdapter context, Writer writer)
throws IOException, MethodInvocationException, ParseErrorException, ResourceNotFoundException { int i, k = jjtGetNumChildren(); for (i = 0; i < k; i++)
jjtGetChild(i).render(context, writer); return true;
}
不同类型子节点渲染方法不一样,下面看下ASTText类型,可知只是简单的把文本写入writer:
public boolean render( InternalContextAdapter context, Writer writer)
throws IOException {
writer.write(ctext); return true;
}
再看下有子节点ASTmethod的ASTReference的渲染:
ASTReference.render()
public boolean render(InternalContextAdapter context, Writer writer) throws IOException,
MethodInvocationException {
...
{ //执行execute方法
value = execute(null, context);
}
String localNullString = null;
...
value = EventHandlerUtil.referenceInsert(rsvc, context, literal, value);
String toString = null; if (value != null)
{
if (value instanceof Renderable)
{
Renderable renderable = (Renderable)value; try
{ if (renderable.render(context,writer)) return true;
} catch(RuntimeException e)
{ // We commonly get here when an error occurs within a block reference.
// We want to log where the reference is at so that a developer can easily
// know where the offending call is located. This can be seen
// as another element of the error stack we report to log.
log.error("Exception rendering "
+ ((renderable instanceof Reference)? "block ":"Renderable ")
+ rootString + " at " + Log.formatFileString(this)); throw e;
}
}
toString = value.toString();
}
...
{ //person.sayHello()结果写入writer
writer.write(escPrefix);
writer.write(morePrefix);
writer.write(toString); return true;
}
}
ASTReference.execute public Object execute(Object o, InternalContextAdapter context)
throws MethodInvocationException {
... //获取person对象
Object result = getVariableValue(context, rootString); try
{
Object previousResult = result;
int failedChild = -1; for (int i = 0; i < numChildren; i++)
{
...
previousResult = result; //递归解析,调用AstMethod.execute()反射调用person.sayHello();
result = jjtGetChild(i).execute(result,context); if (result == null && !strictRef) // If strict and null then well catch this
// next time through the loop
{
failedChild = i; break;
}
}
... return result;
} catch(MethodInvocationException mie)
{
mie.setReferenceName(rootString); throw mie;
}
}
```Java ASTmethod.execute
public Object execute(Object o, InternalContextAdapter context)
throws MethodInvocationException
{
Object [] params = new Object[paramCount];
final Class[] paramClasses =
paramCount > 0 ? new Class[paramCount] : ArrayUtils.EMPTY_CLASS_ARRAY;
for (int j = 0; j < paramCount; j++)
{
params[j] = jjtGetChild(j + 1).value(context);
if (params[j] != null)
{
paramClasses[j] = params[j].getClass();
}
}
VelMethod method = ClassUtils.getMethod(methodName, params, paramClasses,
o, context, this, strictRef);
if (method == null) return null;
try
{
//反射调用person.sayHello()
Object obj = method.invoke(o, params);
if (obj == null)
{
if( method.getReturnType() == Void.TYPE)
{
return "";
}
}
return obj;
}
....
}
同理看下ASTIdentifier.execute
```java
public Object execute(Object o, InternalContextAdapter context)
throws MethodInvocationException
{
VelPropertyGet vg = null;
try
{
/*
* first, 是否在缓存里面.
*/
IntrospectionCacheData icd = context.icacheGet(this);
if ( icd != null && (o != null) && (icd.contextData == o.getClass()) )
{
vg = (VelPropertyGet) icd.thingy;
}
else
{
//自省获取,默认开启缓存
vg = rsvc.getUberspect().getPropertyGet(o,identifier, uberInfo);
if (vg != null && vg.isCacheable() && (o != null))
{
icd = new IntrospectionCacheData();
icd.contextData = o.getClass();
icd.thingy = vg;
context.icachePut(this,icd);
}
}
}
...
try
{ //反射调用get方法
return vg.invoke(o);
}
。。。
}
另外ASTIdentifier.execute中的 rsvc.getUberspect().getPropertyGet(o,identifier, uberInfo);的逻辑有必要单独说下:
总结:velocity渲染引擎首先磁盘加载模板文件到内存,然后解析模板模板文件为AST结构,并对AST中每个节点进行初始化,第二次加载同一个模板文件时候如果开启了缓存则直接返回模板资源,通过使用资源缓存节省了从磁盘加载并重新解析为AST的开销。
然后配合context里面的变量值深度变量渲染AST节点到writer,对应TExt节点直接写入writer,对应引用节点则先从context获取对象实例,然后通过反射调用指定的方法,调用方法时候没有缓存,每调用一次就反射一次,但是使用对象.属性名方式第一次要使用自省功能找到getMethod,然后在反射调用,但是第二次调用同一个属性时候由于使用了缓存就省去了自省的过程,但是反射还是要的。所以在编写velocity模板时候尽可能使用临时变量保存反射调用结果,减少反射调用次数,降低页面渲染时间。
另外如果开启了资源缓存,并且file.resource.loader.modificationCheckInterval >0还会实现hot deploy也就是会每隔一段时间从磁盘获取最新的模板,重新生成AST结构,即使使用了缓存。