jfinal的默认插件里面是没有redis的,所以用了扒皮的Jfinal-ext(扒皮好像已经好久没有更新了)。里面的redis缓存用法只是按照SQL语句对查询结果进行缓存,感觉没有jfinal中对ehcache插件那样的注解式写法方便和高效。所以自己打算这一个注解式的写法,这样就能保证在拦截器中就直接返回结果,不会再进入到actioin了。
节约字数,就把import和package给省了。JedisKit是jfinal-ext的,Const.CACHE_ACTION_PREFIX就是一个字符串,自定义的,这里是"ACTION-"。
先说一下用法。
1.CacheKey注解可以省略,不省略的话,CacheKey就是"ACTION-ccc"。省略之后会自动生成CacheKey("ACTION-"+请求的路径+url传递的参数),本例中就是"ACTION-/user/a"(此处用了jfinal-ext的自动绑定路径功能)。注:post传递参数不会被记录到CacheKey中,所以尽量不要用post,不过一般需要cache的都是get之类的请求吧。
2.EvictKey注解也可以省略,省略的话就会将这个Controller下的所有cache都清除,本例匹配规则:ACTION-/user*。EvivtKey可以是数组(考虑到可能会出现一个操作影响多个缓存的情况)。
public class UserController extends BaseController<UserModel> {
@Before(CacheAction.class)
@CacheKey("ccc")//可省略
public void a(){
setAttr("beetl", "beetl success");
render("a");
}
@Before(EvictCache.class)
@EvictKey("ccc")//可省略;可以是数组如:@EvictKey({"ccc","bbb"})
public void b(){
setAttr("beetl", "beetl right");
render("a");
}
}
考虑过直接用注解,类似@CacheAction("ccc")这种形式的写法,但是这样的话,就意味着要加一个全局的拦截器,然后所有请求都要判断是否含有@CacheAction。总感觉这样未免小题大做,所以还是用@Before这个高效的注解吧。
下面是源码。
--拦截器CacheAction.java,其实就是修改了jfinal的CacheInterceptor.java。
public class CacheAction implements Interceptor {
private static final String renderKey = "$renderKey$";
private static volatile ConcurrentHashMap<String, ReentrantLock> lockMap = new ConcurrentHashMap<String, ReentrantLock>();
private ReentrantLock getLock(String key) {
ReentrantLock lock = lockMap.get(key);
if (lock != null)
return lock;
lock = new ReentrantLock();
ReentrantLock previousLock = lockMap.putIfAbsent(key, lock);
return previousLock == null ? lock : previousLock;
}
final public void intercept(ActionInvocation ai) {
Controller controller = ai.getController();
String key = buildKey(ai, controller);
Map<String, Object> cacheData = JedisKit.get(key);
if (cacheData == null) {
Lock lock = getLock(key);
lock.lock(); // prevent cache snowslide
try {
cacheData = JedisKit.get(key);
if (cacheData == null) {
ai.invoke();
cacheAction(key, controller);
return ;
}
} finally {
lock.unlock();
}
}
useCacheDataAndRender(cacheData, controller);
}
/**
* 通过post传递参数的请求不能被缓存。
* @param ai
* @param controller
* @return
*/
private String buildKey(ActionInvocation ai, Controller controller) {
CacheKey cacheKey = ai.getMethod().getAnnotation(CacheKey.class);
if (cacheKey != null)
return Const.CACHE_ACTION_PREFIX + cacheKey.value();
StringBuilder sb = new StringBuilder();
sb.append(Const.CACHE_ACTION_PREFIX);
sb.append(ai.getActionKey());
String urlPara = controller.getPara();
if (urlPara != null)
sb.append("/").append(urlPara);
String queryString = controller.getRequest().getQueryString();
if (queryString != null)
sb.append("?").append(queryString);
return sb.toString();
}
private void cacheAction(String key, Controller controller) {
HttpServletRequest request = controller.getRequest();
HashMap<String, Object> cacheData = new HashMap<String, Object>();
for (Enumeration<String> names=request.getAttributeNames(); names.hasMoreElements();) {
String name = names.nextElement();
cacheData.put(name, request.getAttribute(name));
}
cacheData.put(renderKey, controller.getRender()); // cache render
JedisKit.set(key, cacheData);
}
private void useCacheDataAndRender(Map<String, Object> cacheData, Controller controller) {
HttpServletRequest request = controller.getRequest();
Set<Entry<String, Object>> set = cacheData.entrySet();
for (Iterator<Entry<String, Object>> it=set.iterator(); it.hasNext();) {
Entry<String, Object> entry = it.next();
request.setAttribute(entry.getKey(), entry.getValue());
}
request.removeAttribute(renderKey);
controller.render((Render)cacheData.get(renderKey)); // set render from cacheData
}
}
----拦截器EvictCache.java,其实就是修改了jfinal的EvictInterceptor.java。
public class EvictCache implements Interceptor {
final public void intercept(ActionInvocation ai) {
ai.invoke();
JedisKit.del(buildKeys(ai));
}
private String[] buildKeys(ActionInvocation ai) {
EvictKey evictKeys = ai.getMethod().getAnnotation(EvictKey.class);
if (evictKeys != null){
String[] keyArray = evictKeys.value();
for(int i=0; i<keyArray.length; i++){
keyArray[i] = Const.CACHE_ACTION_PREFIX + keyArray[i];
}
return keyArray;
}
StringBuilder sb = new StringBuilder();
sb.append(Const.CACHE_ACTION_PREFIX);
sb.append(ai.getControllerKey());
sb.append("/*");
Set<String> keySet = JedisKit.keys(sb.toString());
String[] keys = keySet.toArray(new String[keySet.size()]);
return keys;
}
}
--CacheKey.java
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface CacheKey {
String value();
}
--EvictKey.java
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface EvictKey {
String[] value();
}
-----------------------------------------------------------------------------
这样弄好之后运行时会出现java.io.NotSerializableException: org.beetl.core.GroupTemplate报错。这是因为模板引擎用了beetl的缘故。
拦截器在进行缓存的时候会把render和request中的参数全都保存起来。jfinal的Render接口是可序列化的,Beetl实现了Render接口,但是和其他的Render不同的是,beetl自带的jfinal插件BeetlRender中有一个属性是GroupTemplate。
public class BeetlRender extends Render
{
GroupTemplate gt = null;
private transient static final String encoding = getEncoding();
private transient static final String contentType = "text/html; charset=" + encoding;
......
......
......
}
这个GroupTemplate没有实现Serializable,所以不能存入redis。并且,如果每个缓存的render里面都有一个GroupTemplate,也会浪费内存,所以还是自己写个BeetlRender为妙。
下面给出我自己的RnBeetlRender和RnBeetlRenderFactory
public class RnBeetlRenderFactory implements IMainRenderFactory {
public static String viewExtension = ".html";
public static GroupTemplate groupTemplate = null;
public RnBeetlRenderFactory() {
if (groupTemplate != null) {
groupTemplate.close();
}
try {
Configuration cfg = Configuration.defaultConfiguration();
WebAppResourceLoader resourceLoader = new WebAppResourceLoader();
groupTemplate = new GroupTemplate(resourceLoader, cfg);
} catch (IOException e) {
throw new RuntimeException("加载GroupTemplate失败", e);
}
}
public Render getRender(String view) {
return new RnBeetlRender(view + viewExtension);
}
public String getViewExtension() {
return viewExtension;
}
}
public class RnBeetlRender extends Render {
private static final long serialVersionUID = 1L;
private transient static final String encoding = getEncoding();
private transient static final String contentType = "text/html; charset=" + encoding;
public RnBeetlRender(String view) {
this.view = view;
}
@Override
public void render() {
try{
response.setContentType(contentType);
WebRender webRender = new WebRender(
RnBeetlRenderFactory.groupTemplate);
webRender.render(view, request, response);
} catch (BeetlException e) {
throw new RenderException(e);
}
}
}
然后在JFinalConfig继承类里面配置即可。
public void configConstant(Constants me) {
me.setMainRenderFactory(new RnBeetlRenderFactory());
}
如有问题和建议,请多多指教。