请问下Engine中的templateCache是不是可以做些优化。

templateCache是不是要做下线程安全及最大cache量的限制。使用IStringSource和getTemplateByString时,因为key是用HashKit.md5(content)。如果内容改变,旧template就会一直存在。现在templateCache是private的,我无法在外部做修改。

评论区

JFinal

2017-06-23 11:23

这确实是个非常好的建议,对于 String 型的 Template 来说,当内容发生变化时,key 值也发生了变化,所以也就找不到原来那个 key 值,无法删除 String 型的 Template 的缓存

目前需要开发者手动 remove 一下 cache,可以采用如下的方式:
1:web 项目之下:RenderManager.me().getEngine().removeTemplateCache(key)
2:非 web 项目,Engine.use(...).removeTemplateCache(key)

JFinal

2017-06-23 11:26

jfinal 将此建议纳入到改进列表,看有没有好的办法解决 String template 的缓存问题,目前能想到的是对 String 型 Template 不做缓存,因为 jfinal template engine 是自己手写的解析器,解析模板的性能比 freemaker、velocity 快五倍,这个解析的时空消耗在大多数应用场景下可以忽略

JFinal

2017-06-23 11:38

创建 template 时的线程安全问题不需要处理,Template 对象并不需要做成单例,如果出现多线程并发同时创建“同一个key" 值的 Template,让后一个创建的线程,在 templateCache.put(key, template) 时覆盖掉前面的就可以了

如果让 templateCache 使用 ConcurrentHashMap 或者使用 synchronized 进行同步,会拉低性能

关键在于 Template 对象大并发情况下,多个线程 parse 出多个“相同 key" 的 Template 无关紧要,在那一时刻多消耗点时空而已

i++

2017-06-23 15:35

@JFinal 手动 remove cache时需要知道key。但我们并不知道无用的key是什么。请问有办法获取templateCache的大小吗。当templateCache等于一定量时,我们用removeAllTemplateCache()也行啊。

i++

2017-06-23 15:50

@JFinal String 型 Template 做缓存还是很有必要的。建议别去掉。即使String 型的不做templateCache。只有文件类型的做templateCache,templateCache也会一直增大的。因为文件(html)模板会重命名,会删除,而且文件模板不会太小。要是怕做先进先出的缓存影响性能,至少做个最大量的限制,超过了就全部删除,总比让他无限增长,内存用尽好。这个最大量让使用者可以自己根据服务器内存情况设置。

JFinal

2017-06-23 17:06

@i++ 你可以通过扩展 Engine 的方式来解决:
public class MyEngine extends engine {
public Template getTemplateByString(String content) {
// 在这里用一个 Map 存放你所有的 key 值
// 然后用一个独立的定时器定时清理一下就可以了
// 清理的时候可以简单性的 remove掉所有 cache,这个不耗什么性能
}
}

随后可以调用 Engine.setMainEngine(new MyEngine()); 的方式替换掉 jfinal 的主 Engine 对象

最后你就可以在任何地方通过 Engine.use() 来获取到你自己的 MyEngine 对象了,当然你也可以自行管理你的 MyEngine 对象

JFinal

2017-06-23 17:09

@i++ 你的建议我会综合考虑,是一个改进的思维方向,但还要考虑更多因素,例如文件型的模板,即便改名称,也一定非常有限,大部分情况下是去改文件型模板中的内容

场景很重要,这个问题主要集中在 String 型的 Template 处理,还有很多思路可以考虑,例如,改进 MemoryStringSource,让其自身可以维护 cache都是可以考虑的方向

JFinal

2017-06-23 17:11

还有一个办法是对 getTemplateByString 添加一个 key 参数,让这个 key 值是固定不变的:
engine.getTempateByString("myTemplate", content);

还需要考虑很多,现在的情况下,对于 String Template 的使用可以暂时自己写点代码扩展

JFinal

2017-06-23 17:18

@i++ 刚添加了一个 Engine.getTemplateCacheSize() 方法,可以去 github.com/jfinal/jfinal 下载最新版本的 jfinal 获取代码,记得用上以后反馈给我

i++

2017-07-06 19:56

@jfinal 不知道这么扩展行不行?帮我看下。因为templateCache 是私有的,在子类里也是无法用的。只能在子类里自己维护一个mainTemplateCache 。所以所有template有关的都要重写。

import java.util.HashMap;
import java.util.Map;

import com.jfinal.kit.HashKit;
import com.jfinal.log.Log;
import com.jfinal.template.Engine;
import com.jfinal.template.EngineConfig;
import com.jfinal.template.Env;
import com.jfinal.template.FileStringSource;
import com.jfinal.template.IStringSource;
import com.jfinal.template.MemoryStringSource;
import com.jfinal.template.Template;
import com.jfinal.template.stat.Parser;
import com.jfinal.template.stat.ast.Stat;


/**
* 描述:自定义Engine
* 优化mainTemplateCache,防止mainTemplateCache内存占用过大。
* @author Robin Zhang
* @created 2017年7月6日 下午5:18:50
*/
public class MainEngine extends Engine {

/**
* 描述:
*/
private static Log log = Log.getLog(MainEngine.class);

/**
* 描述:模板缓存
*/
private static Map mainTemplateCache = new HashMap();

static {
Runnable runnable = new Runnable() {
public void run() {
while (true) {
try {
int len = mainTemplateCache.size();
if(len>200){
mainTemplateCache.clear();
log.debug("MainEngine.mainTemplateCache的模板缓存大于200已被清理。");
} else {
try {
Thread.sleep(60000l);
} catch (InterruptedException e) {
log.error("MainEngine.mainTemplateCache.Thread.sleep()出错:",e);
}
}
} catch (Exception e) {
log.error("MainEngine.mainTemplateCache.保存访问记录缓存到数据库定时任务出错",e);
try {
Thread.sleep(60000l);
} catch (InterruptedException e1) {
log.error("MainEngine.mainTemplateCache.Thread.sleep()出错:",e);
}
}
}
}
};
Thread thread = new Thread(runnable);
thread.start();
}

/**
* 描述:Get template with file name
* @author Robin Zhang
* @created 2017年7月6日 下午5:53:25
* @param fileName
* @return
* @see com.jfinal.template.Engine#getTemplate(java.lang.String)
*/
public Template getTemplate(String fileName) {
if (fileName.charAt(0) != '/') {
char[] arr = new char[fileName.length() + 1];
fileName.getChars(0, fileName.length(), arr, 1);
arr[0] = '/';
fileName = new String(arr);
}

Template template = mainTemplateCache.get(fileName);
if (template == null) {
template = buildTemplateByFileStringSource(fileName);
mainTemplateCache.put(fileName, template);
} else if (getDevMode()) {
if (template.isModified()) {
template = buildTemplateByFileStringSource(fileName);
mainTemplateCache.put(fileName, template);
}
}
return template;
}

/**
* 描述:
* @author Robin Zhang
* @created 2017年7月6日 下午5:54:23
* @param fileName
* @return
*/
private Template buildTemplateByFileStringSource(String fileName) {
EngineConfig config = getEngineConfig();
FileStringSource fileStringSource = new FileStringSource(config.getBaseTemplatePath(), fileName, config.getEncoding());
Env env = new Env(config);
Parser parser = new Parser(env, fileStringSource.getContent(), fileName);
if (getDevMode()) {
env.addStringSource(fileStringSource);
}
Stat stat = parser.parse();
Template template = new Template(env, stat);
return template;
}


/**
* 描述:Get template by string content
* @author Robin Zhang
* @created 2017年7月6日 下午5:54:34
* @param content
* @return
* @see com.jfinal.template.Engine#getTemplateByString(java.lang.String)
*/
public Template getTemplateByString(String content) {
String key = HashKit.md5(content);
Template template = mainTemplateCache.get(key);
if (template == null) {
template = buildTemplateByStringSource(new MemoryStringSource(content));
mainTemplateCache.put(key, template);
} else if (getDevMode()) {
if (template.isModified()) {
template = buildTemplateByStringSource(new MemoryStringSource(content));
mainTemplateCache.put(key, template);
}
}
return template;
}

/**
* 描述:Get template with implementation of IStringSource
* @author Robin Zhang
* @created 2017年7月6日 下午5:54:43
* @param stringSource
* @return
* @see com.jfinal.template.Engine#getTemplate(com.jfinal.template.IStringSource)
*/
public Template getTemplate(IStringSource stringSource) {
String key = stringSource.getKey();
Template template = mainTemplateCache.get(key);
if (template == null) {
template = buildTemplateByStringSource(stringSource);
mainTemplateCache.put(key, template);
} else if (getDevMode()) {
if (template.isModified()) {
template = buildTemplateByStringSource(stringSource);
mainTemplateCache.put(key, template);
}
}
return template;
}

/**
* 描述:
* @author Robin Zhang
* @created 2017年7月6日 下午5:54:53
* @param stringSource
* @return
*/
private Template buildTemplateByStringSource(IStringSource stringSource) {
EngineConfig config = getEngineConfig();
Env env = new Env(config);
Parser parser = new Parser(env, stringSource.getContent(), null);
if (getDevMode()) {
env.addStringSource(stringSource);
}
Stat stat = parser.parse();
Template template = new Template(env, stat);
return template;
}

/**
* 描述:
* @author Robin Zhang
* @created 2017年7月6日 下午5:06:26
* @param templateKey
* @see com.jfinal.template.Engine#removeTemplateCache(java.lang.String)
*/
@Override
public void removeTemplateCache(String templateKey) {
super.removeTemplateCache(templateKey);
mainTemplateCache.remove(templateKey);
}

/**
* 描述:
* @author Robin Zhang
* @created 2017年7月6日 下午5:06:26
* @see com.jfinal.template.Engine#removeAllTemplateCache()
*/
@Override
public void removeAllTemplateCache() {
super.removeAllTemplateCache();
mainTemplateCache.clear();
}

/**
* 描述:获取模板缓存大小
* @author Robin Zhang
* @created 2017年7月6日 下午5:18:03
* @return
*/
public int getTemplateCacheSize() {
return mainTemplateCache.size();
}
}





config.java中注册mainEngine
@Override
public void configEngine(Engine engine) {
Engine.setMainEngine(new MainEngine());
}

JFinal

2017-07-07 11:18

@i++ 我倒是建议你不扩展 Engine,用一个独立的定时器线程每隔一段时间去看看 templateCacheSize(),如果超出一定的范围就清一下缓存,当然这样也会清掉模板文件的缓存

要做得更加完善,通过继承 engine 也是可以的,但不需要那么麻烦,只需要在继承类中覆盖掉 getTemplateByString 方法,然后将 key 存起来,并调用父类就可以了,大致如下:
public class MyEngine extends Engine {
Map stringTemplate = new HashMap();

public Template getTemplateByString(String content) {
String key = HashKit.md5(content);
Template ret = stringTemplate.get(key);
if (ret != null) {
return ret;
}
return super.getTemplateByString(content);
}

public void clearStringCache() {
// 这里对 Map stringTemplate 进行迭代
// 利用其 key 调用父类的 removeCache 方法
super.removeTempateCache(key);
stringTemplate.remove(key);
}
}

JFinal

2017-07-07 11:19

最后再补充一点, jfinal 3.2 已经对 string template 默认进行缓存了,如果要缓存需要这么调用:
engine.getTemplateByString(content, true);

而 engine.getTemplateByString(content) 相当于第二个参数为 false,这个在升级到 3.2 版本的时候很轻松,先知道这件事就好

i++

2017-08-14 15:37

@JFinal 今天看了3.2这部份源码。我并不认同这种把问题抛给使用者以自选的方式处理。原因
1.没有几个使用者会认真区分这两个方法的真正区别,大部份人只知道做下表面的试验,能用就好了。而这个问题在平时的开发使用过程中是很难发现问题的。而当应用服务出现问题时他们的第一反应是:jfinal这个框架不行。谨慎点的开发者看到了注释为了安全起见,他们往往会默认选择不缓存。不缓存性能相对就差点。
2.模板的主要特点就是可重用性,我是真想不到有什么业务场景要用到模板但只执行一次的。既然要使用多次,那么进行缓存肯定是比较好的。所以做为应用的底层默认是要缓存的。
3.这个问题的根源是在缓存方法中默认使用content的md5当key,做为一个开发框架的底层不应该使用这种有局限性的实现方法。所以要让方法通用,应该让使用者自己设置缓存的key。提供默认不缓存engine.getTemplateByString(content) 和默认缓存engine.getTemplateByString(key,content)两个方法。再提供engine.setTemplateCacheMaxSize()设置最大缓存条数,实现当缓存达到最大值时自动清除。或用设置有效期的方式,在jfinal框架本身把这个问题解决掉。而不是让使用者自己选择只能缓存某一类的数据,这样的约定在开发过程中的业务层是很常见,但在开发的框架底层使用这样的约定就会影响到使用业务的范围。

JFinal

2017-08-14 15:56

由于 jfinal 是自己写算法来解析模板,所以解析性能是 freemarker、velocity 的 6 倍,这个性能在绝大多数场景下都是足够的,对于一个普通的模板文件解析时间都只需要几个毫秒,更何况这类 String 的内存对象,解析时间几乎可以忽略不计。

因此,jfinal 3.2 对 String 内存型的模板默认采用了不缓存的策略。而只有在极端情况下才需要缓存,这时候开发者可以使用 getTemplateByString 的 true 参数进行缓存,这种情况适用于 String 型的模板个数是有限的,不会造成内存泄漏

如果对性能有极端要求,同时 String 型模板的个数是不确定的,那么可以通过实现 ISource 接口去解决,该接口中有一个 getKey() 方法,可以用于指定缓存的 key,下面是一个大致的例子:
public class StringTemplate implements ISource {
private String key;
private String content;
public StringTemplate(String key, String content) {
this.key = key;
this.content = content;
}

public String getKey() {
return key;
}
// 其它实现方法省略
}

在用的时候这样:
engine.getTemplate(new StringTemplate(key, content));

JFinal

2017-08-14 16:01

补充一下,添加一个你建议的 Engine.getTemplateByString(key,content) 方法也是可以的,但如我前面所说 engine 默认行为已经能够应对绝大部分场景,并且在极端情况下也可以很方便使用 true 参数,或者扩展 ISource 接口,所以也就没有添加这个方法,在保障大部分场景,又满足了完备性的同时,能省则省了

当然,这个问题一直是保持开放的,将来认知的提升,以及用户需求与反馈的消息过来以后,随时可以添加新的支持,你的建议也随时可以被添加进来

热门反馈

扫码入社