在JFinal中,已经提供了对dao以及action进行快速缓存的方法,就像下面这样:
//使用Model的缓存实现 List<Blog> blogList = Blog.dao.findByCache("cacheName", "key", "select * from blog"); //使用Db的缓存实现 List<Record> blogList = Db.findByCache("cacheName", "key", "select * from blog"); //对action进行缓存实现 @Before(CacheInterceptor.class) @CacheName("blogList") public void list() { List<Blog> blogList = Blog.dao.find("select * from blog"); setAttr("blogList", blogList); render("blog.html"); }
但在实际使用中发现,这两种缓存,前者粒度有点小,后者粒度又有点大
对action做缓存还有一个坑:就是action是有状态的
虽然默认提供的CacheInterceptor在生成cacheKey时已经考虑的很全面了,连request.getQueryString()都全部加进去了,但对session、cookie或者ThreadLocal中的一些变量却无能为力,而在action中往往需要根据这些变量来决定最终的模板渲染效果,所以,为action做缓存几乎是不现实的。
但我们知道,业务逻辑应该是无状态的,任何会话变量都不应该在业务层出现,甚至ThreadLocal也应该由调用者传入,而不应该在业务层的方法内部直接获取。
所以我认为对业务层进行cache是最合适的,其实spring也是这么做的
研究了一下,经过小小的处理,在JFinal中也可以很方便的对Service方法进行注解方式的缓存实现。
1、创建一个Cache注解类:
/** * 业务逻辑缓存注解 * * @author netwild@qq.com * */ public @interface Cache { /** * 对被标注的方法启用默认的缓存实现 * * 参数: * * cache:cacheName,可选,为空时使用Class+Method+ParameterType自动生成 * key: cacheKey,可选,为空时使用args.hashCode自动生成 * * @author netwild@qq.com * */ @Inherited @Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.METHOD }) public static @interface able{ String cache() default ""; String key() default ""; } /** * 仅将方法的返回值添加到缓存 * * 具体调用同Cache.able * * @author netwild@qq.com * */ @Inherited @Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.METHOD }) public static @interface put{ String cache() default ""; String key() default ""; } /** * 从缓存中移除对象 * * 具体调用同Cache.able * * @author netwild@qq.com * */ @Inherited @Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.METHOD }) public static @interface del{ String cache() default ""; String key() default ""; } }
2、创建一个业务层拦截器:
/** * 业务层拦截器:缓存实现 * * @author netwild@qq.com * */ public class CacheInterceptor implements Interceptor { private final static Log log = Log.getLog(CacheInterceptor.class); @Override public void intercept(Invocation inv) { /** * 初始化缓存配置对象 */ ServiceCacheItem cacheItem = new ServiceCacheItem(inv); /** * 如果允许使用缓存,则尝试直接从缓存返回方法执行结果 */ if(cacheItem.isAble && ECacheKit.has(cacheItem.cacheName, cacheItem.cacheKey)){ inv.setReturnValue(ECacheKit.get(cacheItem.cacheName, cacheItem.cacheKey)); log.info("命中业务缓存数据:{} - {}", cacheItem.cacheName, cacheItem.cacheKey); return; //直接返回,不执行方法体 } /** * 未命中缓存,执行方法体,并保存执行结果 * 注:如果方法执行过程中抛出错误,则直接跳出 */ inv.invoke(); if(cacheItem.hasReturn) cacheItem.cacheValue = inv.getReturnValue(); /** * 如果允许使用缓存,则将本次执行结果加入缓存,等待下次命中 */ if(cacheItem.isAble){ ECacheKit.put(cacheItem.cacheName, cacheItem.cacheKey, cacheItem.cacheValue); log.info("添加新的业务缓存数据:{} - {}", cacheItem.cacheName, cacheItem.cacheKey); } /** * 如果为删除指令,则将缓存数据清除 */ if(cacheItem.isDel){ ECacheKit.remove(cacheItem.cacheName, cacheItem.cacheKey); log.info("删除业务缓存数据:{} - {}", cacheItem.cacheName, cacheItem.cacheKey); } /** * 如果为插入指令,则将执行结果加入缓存 */ if(cacheItem.isPut){ ECacheKit.put(cacheItem.cacheName, cacheItem.cacheKey, cacheItem.cacheValue); log.info("添加新的业务缓存数据:{} - {}", cacheItem.cacheName, cacheItem.cacheKey); } } /** * 缓存对象 * * @author netwild@qq.com * */ class ServiceCacheItem{ Method method; //被拦截的方法体对象 Object[] args; //实参数组 boolean hasReturn = false; //是否有返回值 String cacheName = null; String cacheKey = null; String cacheKeyExpr = null; //cacheKey表达式 Object cacheValue = null; boolean isAble = false; boolean isDel = false; boolean isPut = false; /** * 构造方法 * @param inv */ ServiceCacheItem(Invocation inv){ method = inv.getMethod(); args = inv.getArgs(); hasReturn = !method.getReturnType().getName().equals("void"); checkCacheAble(); checkCacheDel(); checkCachePut(); if(isAble || isDel || isPut){ buildCacheName(); buildCacheKey(); } } /** * 检测是否有 @Cache.able * @return */ void checkCacheAble(){ if(hasReturn && method.isAnnotationPresent(Cache.able.class)){ Cache.able anno = method.getAnnotation(Cache.able.class); if(EStr.notEmpty(anno.cache())) cacheName = anno.cache(); if(EStr.notEmpty(anno.key())) cacheKeyExpr = anno.key(); isAble = true; } } /** * 检测是否有 @Cache.del * @return */ void checkCacheDel(){ if(method.isAnnotationPresent(Cache.del.class)){ Cache.del anno = method.getAnnotation(Cache.del.class); if(EStr.notEmpty(anno.cache())) cacheName = anno.cache(); if(EStr.notEmpty(anno.key())) cacheKeyExpr = anno.key(); isDel = true; } } /** * 检测是否有 @Cache.put * @return */ void checkCachePut(){ if(hasReturn && method.isAnnotationPresent(Cache.put.class)){ Cache.put anno = method.getAnnotation(Cache.put.class); if(EStr.notEmpty(anno.cache())) cacheName = anno.cache(); if(EStr.notEmpty(anno.key())) cacheKeyExpr = anno.key(); isPut = true; } } /** * 如果未指定CacheName,则根据方法的完整路径生成默认CacheName * 注:CacheKit中要实现动态生成CacheName的功能 * @param method * @return */ void buildCacheName(){ if(cacheName == null){ cacheName = EClass.getMethodFullPath(method); } } /** * 如果未指定CacheKey,则根据Method的参数列表HashCode生成CacheKey * 否则解析CacheKey的表达式 * * @param method * @return */ @SuppressWarnings("unchecked") void buildCacheKey(){ if(cacheKeyExpr == null){ cacheKey = Arrays.stream(args).map(arg -> arg.hashCode() + "").collect(Collectors.joining(",")); }else{ Map<String, Object> context = Kv.create(); //实参列表 final Parameter[] params = method.getParameters(); //形参列表 for(int i=0, len=params.length; i<len; i++){ context.put(params[i].getName(), args[i]); } String key = El.me.exec(cacheKeyExpr, context); cacheKey = key; } } } }
补充:执行表达式解析的工具类:El
只是简单对Enjoy进行了封装,真是即方便又强大的好宝贝!
/** * 表达式解析 * @author netwild@qq.com * */ public class El { public final static El me = new El(); private Engine engine; private El(){ engine = new Engine(); } /** * 执行表达式 * @param expression 表达式 * @return */ public String exec(String expression){ return exec(expression, null); } /** * 执行表达式 * @param expression 表达式 * @param context 上下文参数表 * @return */ public String exec(String expression, Map<String, Object> context){ Template template = engine.getTemplateByString(expression); String ret = template.renderToString(context); return ret; } }
3、在Config中添加全局业务层拦截器:
public void configInterceptor(Interceptors me) { me.addGlobalServiceInterceptor(new CacheInterceptor()); //业务层拦截器 }
4、创建一个Service试一下:
/** * 单位管理服务 * @author netwild@qq.com * */ public class CompService { /** * 必须使用Enhancer对业务类进行增强,即创建代理类,并实现单列模式 * 这样业务类被调用时,才会被全局拦截器捕获 */ public final static CompService me = Enhancer.enhance(CompService.class); private CompService(){} /** * 定义相关的Dao */ private final Comp compDao = new Comp().dao(); /** * cache为空,则拦截器自动生成cacheName,例如“ com.xxx.CompService.findByUser1(com.xxx.User) ” * key也为空,则拦截器自动将获取所有实参的hashCode()拼接成String作为cacheKey */ @Cache.able public Comp findByUser1(User user) { Comp comp = compDao.findFirst("where id=? and used=1", user.getId()); return comp; } /** * cache不为空,则使用自定义的cacheName * key为空,则按照默认规则自动生成cacheKey */ @Cache.able(cache="CompService.findByUser2") public Comp findByUser2(User user) { Comp comp = compDao.findFirst("where id=? and used=1", user.getId()); return comp; } /** * cache不为空,则使用自定义的cacheName * key也不为空,则将表达式结果解析之后的值作为cacheKey */ @Cache.able(cache="CompService.findByUser2", key="#(user.id)") public Comp findByUser3(User user) { Comp comp = compDao.findFirst("where id=? and used=1", user.getId()); return comp; } }
到这里,@Cache.able、@Cache.del和@Cache.put三个注解缓存就都实现了,虽然没有spring的ioc那么彻底,但对于我这种既不想把spring集成进JFinal,但又想使用这种注解方式的业务层缓存还是很实用的。
以上代码只是一个尝试,对各种空指针的判断还不太完善,如果要线上使用,还需要更谨慎的进行一些优化。
最后,以上代码要求JDK8,因为6或者7不支持获取method的parameters,无法实现cacheKey表达式。
1:Service 层应用 cache 的逻辑推理十分合理
2:拦截器中灵活运用了inv.getArgs() 得到用于生成 cache key 的参数值
3:拦截器中灵活运用了 inv.setReturnValue(...) 为业务层设置返回值
4:拦截器中使用 getMethod().getAnnotation(...) 配合自定义注解实现缓存的配置
5:注解配置中使用 enjoy 引擎表达式动态生成结果
6:简洁、优雅、完整的使用 jfinal 各种功能在业务层实现了缓存
这个缓存实现方案非常具有参考价值,建议小伙伴们用在自己的项目中,感谢分享