JFinal

登录 注册

在JFinal中实现类似spring对业务逻辑的自动缓存

在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表达式。

评论

  • 03-01 00:30
    非常高水平的分享,有很多值得学习的亮点:
    1:Service 层应用 cache 的逻辑推理十分合理
    2:拦截器中灵活运用了inv.getArgs() 得到用于生成 cache key 的参数值
    3:拦截器中灵活运用了 inv.setReturnValue(...) 为业务层设置返回值
    4:拦截器中使用 getMethod().getAnnotation(...) 配合自定义注解实现缓存的配置
    5:注解配置中使用 enjoy 引擎表达式动态生成结果
    6:简洁、优雅、完整的使用 jfinal 各种功能在业务层实现了缓存

    这个缓存实现方案非常具有参考价值,建议小伙伴们用在自己的项目中,感谢分享
  • 03-01 08:37
    @JFinal
    本身这个方案里一些使用技巧也是在jfinal.com社区学到的,所以有了好的想法自然要回来跟大家分享。
    ps. 波总睡的竟然比我还晚,佩服一下!
  • 03-01 14:58
    厉害,El有个ElKit,我特意给波总提的需求,哈哈哈~
  • 03-01 15:22
    @Dreamlu 才知道有这个ElKit,以前不起眼没注意哈哈
  • 04-26 11:09
    如果是eclipse开发的话,除了编译版本要1.8,还需要打开javac -parameters,具体看http://www.jb51.net/article/103425.htm 请叫我雷轰
  • 发送