在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 各种功能在业务层实现了缓存
这个缓存实现方案非常具有参考价值,建议小伙伴们用在自己的项目中,感谢分享