论坛首页 Java企业应用论坛

Flex + Java 中小型项目的代码结构研究(二)(如何在业务层管理你的Cache)

浏览 4867 次
精华帖 (0) :: 良好帖 (0) :: 新手帖 (0) :: 隐藏帖 (0)
作者 正文
   发表时间:2008-02-20  
Flex Structure 2

如何在业务层管理你的Cache

上次初步研究了一下前台与后台的关系,但还遗留了一个Server端的Cache问题。

关键字:EHCache, Spring Interceptor, Spring Advice, Java Annotation

前言
在看过很多的Cache的文章和讨论后,我是这样使用Cache的
1. 在Session的生命周期内使用Hibernate的First Level Cache来缓存对象(数据访问层,细粒度缓存)
2. 使用EHCache对Value Object在业务层做缓存(粗粒度缓存,写代码实现)

为什么我不想使用Hibernate的二级缓存呢?主要有以下几点思考
为了提高它的性能,我们把Cache和持久层关连起来,值得吗?
有必要所有的地方都做Cache吗?这些性能的提升是客户想要的吗?
哪些地方需要做Cache不是只有业务层才知道吗?

关于Hibernate二级缓存详细的介绍,大家还是看看下面几篇文章吧,讲得很好
分析Hibernate的缓存机制
http://www.enet.com.cn/article/2008/0115/A20080115110243.shtml
hibernate二级缓存攻略
http://www.iteye.com/topic/18904
Speed Up Your Hibernate Applications with Second-Level Caching
http://www.devx.com/dbzone/Article/29685/1954?pf=true


在现实生活中,在业务层做Cache又会有一些问题
在业务层需要做Cache的方法里要加上添加Cache或清除Cache的代码,这样不但做起来很麻烦,而且把Cache代码和业务逻辑混杂在一起。
在执行一个方法时,哪些关连的Cache需要被清除。如执行了UserBiz.update(userVO)后,需要清除findAll产生的所有Cache,同时也应该把id相同的findById产生的Cache清除。

下面的文章和代码也就是着重解决上面提到的问题

如附图所示,Spring会为所有的Biz方法加上MethodCacheInterceptor.java和MethodCacheAfterAdvice.java,当方法执行之前,Interceptor会对照Annotation的配置去看此方法的结果需不需要和有没有被Cache,然后决定是否直接从Cache中获得结果(如findAll方法)。而After Advice是在方法执行后决定是否要做一些Cache的清理工作(如update方法)。

具体的Annotation配置方法请参照后面的UserBiz.java

废话和理论还是少说点,上代码才是硬道理

ApplicationContext.xml
	<!-- cache -->
	<bean id="cacheManager" class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean">
		<property name="configLocation">
			<value>classpath:ehcache.xml</value>
		</property>
	</bean>
	
	<bean id="methodCache" class="org.springframework.cache.ehcache.EhCacheFactoryBean">
		<property name="cacheManager">
			<ref local="cacheManager"/>
		</property>
		<property name="cacheName">
			<value>com.novem.common.cache.ehcache.METHOD_CACHE</value>
		</property>
	</bean>
	
	<bean id="methodCacheInterceptor" class="com.novem.common.cache.ehcache.MethodCacheInterceptor">
		<property name="cache">
			<ref local="methodCache" />
		</property>
	</bean>
	
	<bean id="methodCacheAfterAdvice" class="com.novem.common.cache.ehcache.MethodCacheAfterAdvice">
		<property name="cache">
			<ref local="methodCache" />
		</property>
	</bean>

	<bean class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator">
		<property name="beanNames">
			<value>*Biz</value>
		</property>
		<property name="interceptorNames">
			<list>
				<value>methodCacheInterceptor</value>
				<value>methodCacheAfterAdvice</value>
			</list>
		</property>
	</bean>


MethodCacheInterceptor.java
package com.novem.common.cache.ehcache;

import java.io.Serializable;

import net.sf.ehcache.Cache;
import net.sf.ehcache.Element;

import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.util.Assert;

import com.novem.common.cache.annotation.MethodCache;

public class MethodCacheInterceptor implements MethodInterceptor,
	InitializingBean
{
    private Cache cache;

    /**
     * sets cache name to be used
     */
    public void setCache(Cache cache)
    {
	this.cache = cache;
    }

    /**
     * Checks if required attributes are provided.
     */
    public void afterPropertiesSet() throws Exception
    {
	Assert.notNull(cache,
		"A cache is required. Use setCache(Cache) to provide one.");
    }

    /**
     * main method caches method result if method is configured for caching
     * method results must be serializable
     */
    public Object invoke(MethodInvocation invocation) throws Throwable
    {
	// do not need to cache
	if(!invocation.getMethod().isAnnotationPresent(MethodCache.class)
		|| MethodCache.FALSE.equals(invocation.getMethod().getAnnotation(MethodCache.class).isToCache()))
	{
	    return invocation.proceed();
	}
	
	String targetName = invocation.getThis().getClass().getName();
	String methodName = invocation.getMethod().getName();
	Object[] arguments = invocation.getArguments();
	Object result;

	String cacheKey = getCacheKey(targetName, methodName, arguments);
	Element element = cache.get(cacheKey);
	if (element == null)
	{
	    // call target/sub-interceptor
	    result = invocation.proceed();

	    // cache method result
	    element = new Element(cacheKey, (Serializable) result);
	    cache.put(element);
	}
	return element.getValue();
    }

    /**
     * creates cache key: targetName.methodName.argument0.argument1...
     */
    private String getCacheKey(String targetName, String methodName,
	    Object[] arguments)
    {
	StringBuffer sb = new StringBuffer();
	sb.append(targetName).append(".").append(methodName);
	if ((arguments != null) && (arguments.length != 0))
	{
	    for (int i = 0; i < arguments.length; i++)
	    {
		sb.append(".").append(arguments[i]);
	    }
	}

	return sb.toString();
    }
}


MethodCacheAfterAdvice.java
package com.novem.common.cache.ehcache;

import java.lang.reflect.Method;
import java.util.List;

import net.sf.ehcache.Cache;

import org.springframework.aop.AfterReturningAdvice;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.util.Assert;

import com.novem.common.cache.annotation.CacheCleanMethod;
import com.novem.common.cache.annotation.MethodCache;

public class MethodCacheAfterAdvice implements AfterReturningAdvice,
	InitializingBean
{
    private Cache cache;

    public void setCache(Cache cache)
    {
	this.cache = cache;
    }

    public MethodCacheAfterAdvice()
    {
	super();
    }

    public void afterReturning(Object returnValue, Method method,
	    Object[] args, Object target) throws Throwable
    {
	// do not need to remove cache
	if (!method.isAnnotationPresent(MethodCache.class)
		|| method.getAnnotation(MethodCache.class).cacheCleanMethods().length == 0)
	{
	    return;
	}
	else
	{
	    String targetName = target.getClass().getName();

	    CacheCleanMethod[] cleanMethods = method.getAnnotation(
		    MethodCache.class).cacheCleanMethods();
	    List list = cache.getKeys();
	    for (int i = 0; i < list.size(); i++)
	    {
		for (int j = 0; j < cleanMethods.length; j++)
		{
		    String cacheKey = String.valueOf(list.get(i));

		    StringBuffer tempKey = new StringBuffer();
		    tempKey.append(targetName);
		    tempKey.append(".");
		    tempKey.append(cleanMethods[j].methodName());

		    if (CacheCleanMethod.CLEAN_BY_ID.equals(cleanMethods[j].cleanType()))
		    {
			tempKey.append(".");
			tempKey.append(getIdValue(target, method, args[0]));
		    }

		    if (cacheKey.startsWith(tempKey.toString()))
		    {
			cache.remove(cacheKey);
		    }
		}
	    }
	}
    }

    private String getIdValue(Object target, Method method, Object idContainer)
    {
	String targetName = target.getClass().getName();

	// get id value
	String idValue = null;
	if (MethodCache.TRUE.equals(method.getAnnotation(MethodCache.class)
		.firstArgIsIdContainer()))
	{
	    if (idContainer == null)
	    {
		throw new RuntimeException(
			"Id container cannot be null for method "
				+ method.getName() + " of " + targetName);
	    }

	    Object id = null;
	    try
	    {
		Method getIdMethod = idContainer.getClass().getMethod("getId");
		id = getIdMethod.invoke(idContainer);
	    }
	    catch (Exception e)
	    {
		throw new RuntimeException("There is no getId method for "
			+ idContainer.getClass().getName());
	    }

	    if (id == null)
	    {
		throw new RuntimeException("Id cannot be null for method "
			+ method.getName() + " of " + targetName);
	    }
	    idValue = id.toString();
	}
	else if (MethodCache.TRUE.equals(method
		.getAnnotation(MethodCache.class).firstArgIsId()))
	{
	    if (idContainer == null)
	    {
		throw new RuntimeException("Id cannot be null for method "
			+ method.getName() + " of " + targetName);
	    }
	    idValue = idContainer.toString();
	}

	return idValue;
    }

    public void afterPropertiesSet() throws Exception
    {
	Assert.notNull(cache,
		"Need a cache. Please use setCache(Cache) create it.");
    }

}


MethodCache.java
package com.novem.common.cache.annotation;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@Retention(RetentionPolicy.RUNTIME)
public @interface MethodCache
{
    String TO_CACHE = "TO_CACHE";
    String NOT_TO_CACHE = "NOT_TO_CACHE";
    
    String TRUE = "TRUE";
    String FALSE = "FALSE";
    
    public String isToCache() default TO_CACHE;
    
    public String firstArgIsId() default FALSE;
    
    public String firstArgIsIdContainer() default FALSE;
    
    public CacheCleanMethod[] cacheCleanMethods() default {};
}


CacheCleanMethod.java
package com.novem.common.cache.annotation;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@Retention(RetentionPolicy.RUNTIME)
public @interface CacheCleanMethod
{
    String CLEAN_ALL = "CLEAN_ALL";
    String CLEAN_BY_ID = "CLEAN_BY_ID";
    
    public String methodName();
    
    public String cleanType() default CLEAN_ALL;
}



UserBiz.java
package com.novem.farc.biz;

import java.util.List;

import com.novem.common.cache.annotation.CacheCleanMethod;
import com.novem.common.cache.annotation.MethodCache;
import com.novem.farc.vo.UserVO;

public interface UserBiz
{
    @MethodCache()
    public UserVO findById(Long id);

    @MethodCache()
    public List findAll(int firstResult, int maxResults);

    @MethodCache(
	    isToCache = MethodCache.FALSE,
	    firstArgIsIdContainer = MethodCache.TRUE,
	    cacheCleanMethods = {@CacheCleanMethod(methodName="findById", cleanType = CacheCleanMethod.CLEAN_BY_ID),
		    @CacheCleanMethod(methodName="findAll")}    
    )
    public void update(UserVO vo);

    @MethodCache(
	    isToCache = MethodCache.FALSE,
	    firstArgIsIdContainer = MethodCache.TRUE,
	    cacheCleanMethods = {@CacheCleanMethod(methodName="findAll")}    
    )
    public Long insert(UserVO vo);

    @MethodCache(
	    isToCache = MethodCache.FALSE,
	    firstArgIsId = MethodCache.TRUE,
	    cacheCleanMethods = {@CacheCleanMethod(methodName="findById", cleanType = CacheCleanMethod.CLEAN_BY_ID),
		    @CacheCleanMethod(methodName="findAll")}    
    )
    public void remove(Long id);
}


注意:如果@CacheCleanMethod的cleanType = CacheCleanMethod.CLEAN_BY_ID,则此方法的第一个参数一定要是对象的ID(userId)或ID container(UserVO, 并且此对象中要有getId方法)。之所以要有这样的限制,我是觉得在企业开发中,大家follow这样的规则就好,没必要为了能灵活地取出ID再多搞出一些配置项出来。


为什么我不使用XML来配置Cache呢?

下面是我最早的时候写的一个配置XML,但后来发现,使用这种方法,就得为每一个Biz配置一个XML,就和早期的xxx.hbm.xml一样,管理起来比较麻烦,不如Annotation简洁

UserBiz.cache.xml
<ehcache>
	<bean name="com.novem.farc.biz.UserBiz">
		<method name="findById"/>
		<method name="findAll"/>
		<method name="update">
			<cleanList>
				<method name="findAll"/>
				<method name="findById" type="exactly"/>
			</cleanList>
		</method>
		<method name="insert">
			<cleanList>
				<method name="findAll"/>
			</cleanList>
		</method>
		<method name="delete">
			<cleanList>
				<method name="findAll"/>
				<method name="findById" type="exactly"/>
			</cleanList>
		</method>
	</bean>
</ehcache>
  • 大小: 15.5 KB
   发表时间:2008-02-20  
我从写这些代码开始就有一个疑问,前面有OSCache, EHCache等等Framework来帮忙管理Cache,后面有Spring帮忙配置哪些方法需要做Cache。

那中间呢,为什么没有一些小的东东来帮忙配置如何管理Cache呢?Spring为什么不做这个?有什么问题吗?还是出于什么样的考虑?

希望了解这个的朋友能提点一二:)
0 请登录后投票
   发表时间:2008-02-21  
突然想起来,之前Robin写过一篇文章说是要细粒度Cache,和我这里的思路有点不太一样,
如果没记错,他使用的是Ruby+Hibernate,所以他不要VO层的Cache,另外一点,就是他做的Cache比较全面,而我在这里用的只是粗粒度的,对于一个企业应用来说,有时候性能不会像JavaEye这么重要,考虑得多点的可能是工作量

大家有兴趣的可以把他那篇文章找出来读读
0 请登录后投票
   发表时间:2008-03-20  
没有想到cache 是这样用的,受教育了。
谢谢
0 请登录后投票
   发表时间:2008-04-01  
有个朋友发消息给我,问了这两个问题,我觉得比较典型,所以就放上来了

1.如果项目很小,仅仅局域网使用,访问人数不是很多,同时在线不会超过100,是否有必要使用cache 和lazyload。
不要为了用Cache而去用Cache,只有当你在使用系统,或预计到会有性能问题的时候才去用它。
这篇文章中,我也介绍了怎么通过配置的方式使用Cache,代码和说明上面都有

这里的lazyload是为了让客户端的数据结构比较完整,而且同时避免加载不必要的数据

2.Model Locator 您在项目中否使用?我在demo中当session使用,如果不使用,您通过什么保存状态。

我之前有个项目也用了,也确实出现过一些bug
我建议能不用的地方就不要去用它,就像你在写C的时候也不会用很多的全局变量一样
要想保存状态,在各自的对象里面去保存,实在没办法才去用Model Locator
0 请登录后投票
   发表时间:2008-08-04  
一般来说缓存越接近前端效果越明显。比如jsp cache (html) > service > dao (hibernate二级缓存) > 数据库(oracle自身的缓存)。
但对应地,缓存越接近前端越难控制。hibernate二级缓存要求对数据库的独占。
显然,楼主这种service 缓存方法不但对数据库的独占,还要求对dao的独占,否则会导致读到的是脏数据。
还要注意一点使用了service 缓存就要保证入口的单一性,那就要求每个类的责任很明确了,A类做的事不能由B类去完成了,这需要域领域的分析能力。在公司里人员水平参差不齐,很难保证这点,所以service 缓存要小心。
就算能很好区分好各类的责任,也会很难控制service 缓存的正确性,时间一久,代码一多,什么都忘了。
如果只用service 缓存来处理保存,修改这种简单的操作,还不如直接用hibernate二级缓存。
互联网应用和企业应用有着很大的不同。互联网应用对数据的实时性不是很敏感。可以做些很接近前端的缓存,如页面静态化。
企业应用往往很关注数据的正确性。

个人认为声明也是代码,所以认为楼主的方法一样是Cache代码和业务逻辑写在一起,声明的效果相当于把Cache公用处理代码写在公用Util类。

以上是个人看法,大家多多讨论。
0 请登录后投票
   发表时间:2008-10-20  
哦,只有JAVA端(Cache)的设计。兴趣不大。啥时候来个FLEX端的设计?
0 请登录后投票
论坛首页 Java企业应用版

跳转论坛:
Global site tag (gtag.js) - Google Analytics