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()); }
如有问题和建议,请多多指教。