论坛首页 Java企业应用论坛

aop cache再讨论

浏览 21799 次
该帖已经被评为良好帖
作者 正文
   发表时间:2008-11-07   最后修改:2009-05-19
/**
*作者:张荣华
*日期:2008-11-07
**/

开门见山,一刀见血,让我们说说烦人的aop cache.

aop cache解释使用aop技术的cache,可以cache被代理对象的方法返回结果,还可以通过方法的参数值来控制缓存的粒度,看上去很美,用的人估计也颇多,好东西啊,面试的时候经常有人告诉我"我用过aop cache",看来是居家必备啊.不过居家必备的东西也得升个级什么滴啊,就想汽车一样,每年拉一次皮,照卖,还自夸是新一袋.aop cache要升级得先看看它烦人得地方.看看它烦人得地方先得知道它得用法,那么就先简单介绍一下它得用法:

常见步骤,2步
1,建立一个拦截器类,环绕增强或者后增强都可以,代码如下:
/**
 * @author ahuaxuan(aaron zhang) 代码原主是一个老外,不是我
 * @since 2008-5-13
 * @version $Id: MethodCacheInterceptor.java 814 2008-05-13 06:52:54Z aaron $
 */
@Component("methodCacheInterceptor")
@GlobalAutowired//这个是俺写的globalautowired,大家可以忽略
public class MethodCacheInterceptor implements MethodInterceptor {

	private Cache methodCache;

	public void setMethodCache(Cache methodCache) {
		this.methodCache = methodCache;
	}

	public Object invoke(MethodInvocation invocation) throws Throwable {
		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 = methodCache.get(cacheKey);
		if (element == null) {
			result = invocation.proceed();

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

	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();
	}

}


这段代码很简单,就是缓存某个方法的返回结果,使用的缓存组件是ehcache,ehcache的比较详细的用法ahuaxuan在http://www.iteye.com/topic/128458这篇文章中已经有了说明.

而配置自动代理:
<!-- method cache auto proxy, add by ahuaxuan -->
     <bean class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator">
         <property name="beanNames">
              <list>
                   <value>aaComponent</value>
                   <value>bbComponent</value>
              </list>
         </property>
         <property name="interceptorNames">
              <list>
                   <value>methodCacheInterceptor</value>
              </list>
         </property>
     </bean>

Over,最简单的aop cache.使用了该aop cache之后,可以缓存方法返回结果于无形,又可以根据方法参数来控制缓存粒度, 实乃居家旅行,杀人越货的必备良药

那么接下来看看这个用法有没有什么问题,相信熟悉一点的童子一眼就看出来了:”糟了,aaComponent和bbComponent所有的方法都被拦截了”.这个代码着实让我焦虑,我很焦虑.

Ok,我改,我改正则表达式还不行吗,我可以通过正则表达式让某些特定方法名的方法才被拦截处理.好啊,正统的spring用法,于是advice变成了advisor,增强变成了增强器, 但是我怎么看着就这么扭呢,难道我要缓存一个方法的结果还非得把这个方法的名字按照某个固定的格式来取, 再着,两个get方法,一个getxxx(),一个getyyy,两个之中一个需要缓存,另外一个不需要缓存(靠,真是变态),怎么办呢?正则的方式让我很烦躁,非常烦躁.

第一种方法让我焦虑,而第二种方法让我烦躁,我应该去寻找解决焦虑和烦躁的方案.

写代码需要有灵感,也需要有很强的分析能力,我们来看看我的问题是什么:
问题重新描述:不能精确的控制某个对象的某个方法需要被缓存.
思考:如何固定这个方法的标示-------------------
hardcode方法名到methodinterceptor中
hardcode缓存标示到方法上(如果该类所有方法都需要被缓存,那么hardcode缓存标示到类上)

我选第二种.那么看看实现步骤:
1.annotation类,两个:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MethodCache {

}
还有一个:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface ObjectCache {

}

看上去是多么无聊的两个annotation.

2修改methodinterceptor,加上判断逻辑
如果被代理的类加了ObjectCache,那么拦截这个对象所有的方法,如果没有类上没有加ObjectCache,那么判断method上有没有加methodcache,如果加了,拦截该方法,如果没有加,直接调用目标类的方法.

于是代码变成:
public Object invoke(MethodInvocation invocation) throws Throwable {
		
		String targetName = invocation.getThis().getClass().getInterfaces()[0].getName();
		String methodName = invocation.getMethod().getName();
		Object[] arguments = invocation.getArguments();
		
		if (invocation.getThis().getClass().isAnnotationPresent(ObjectCache.class)) {
			return getResult(targetName, methodName, arguments, invocation);
		} else {
			if (invocation.getMethod().isAnnotationPresent(MethodCache.class)) {
				return getResult(targetName, methodName, arguments, invocation);
			} else {
				return invocation.proceed();
			}
		}
	}

private Object getResult(String targetName, String methodName, Object[] arguments, MethodInvocation invocation) throws Throwable {
		Object result;
		
		String cacheKey = getCacheKey(targetName, methodName, arguments);
		Element element = methodCache.get(cacheKey);
		if (element == null) {
			result = invocation.proceed();

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

Ok,试试把,现在我要拦截aaservice上所有的方法,那么我的代码如下:
@ObjectCache
public class AaService  implement xxxxxx{

}


如果我要拦截bbservice上的b1方法,代码如下:
public class BbService implement xxxxxx{

	@MethodCache
	public void bb() {
		
	}
}


好了,目的达到了,我们可以任意的指定需要要拦截某个类的全部,或者部分方法了. 可是心中好像还是很闷的慌,我很慌张,非常慌张.
有人问了:都到这个份上了还慌啥张啊.
答:它tmd什么时候过期啊.我在ehcache.xml配置的可是统一的过期时间啊.ok,想到了,改,于是俺们的annotation就长成下面这个样子了:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MethodCache {
	int expire() default 0;
}



@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface ObjectCache {
	int expire() default 0;
}


再看看我们的进化过的methodInterceptor吧,大家可以详细比较一下下面这段和上面两端代码的异同之处
public Object invoke(MethodInvocation invocation) throws Throwable {
		
		String targetName = invocation.getThis().getClass().getInterfaces()[0].getName();
		String methodName = invocation.getMethod().getName();
		Object[] arguments = invocation.getArguments();
		Class[] cs = new Class[arguments.length];
		for (int k = 0; k < arguments.length; k++) {
			cs[k] = arguments[k].getClass();
		}
		
		if (invocation.getThis().getClass().getCanonicalName().contains("$Proxy")) {
			if (logger.isWarnEnabled()) {
				logger.warn("----- The object has been proxyed and method " +
						"cache interceptor can't get the target, " +
						"so the method result can't be cached which name is ------" + methodName);
			}
			
			return invocation.proceed();
		} else {
			if (invocation.getThis().getClass().isAnnotationPresent(ObjectCache.class)) {
				ObjectCache oc = invocation.getThis().getClass().getAnnotation(ObjectCache.class);
				return getResult(targetName, methodName, arguments, invocation, oc.expire());
			} else {
				
				Method[] mss = invocation.getThis().getClass().getMethods();
				Method ms = null;
				for (Method m : mss) {
					if (m.getName().equals(methodName)) {
						boolean argMatch = true;
						Class[] tmpCs = m.getParameterTypes();
						if (tmpCs.length != cs.length) {
							argMatch = false;
							continue;
						}
						for (int k = 0; k < cs.length; k++) {
							if (!cs[k].equals(tmpCs[k])) {
								argMatch = false;
								break;
							}
						}
						
						if (argMatch) {
							ms = m;
							break;
						}
					}
				}
				
				if (ms != null && ms.isAnnotationPresent(MethodCache.class)) {
					MethodCache mc = ms.getAnnotation(MethodCache.class);
					return getResult(targetName, methodName, arguments, invocation, mc.expire());
				} else {
					return invocation.proceed();
				}
			}
		}
	}
	
	private Object getResult(String targetName, String methodName, Object[] arguments,
			MethodInvocation invocation, int expire) throws Throwable {
		Object result;
		
		String cacheKey = getCacheKey(targetName, methodName, arguments);
		Element element = methodCache.get(cacheKey);
		if (element == null) {
			synchronized (this) {
				element = methodCache.get(cacheKey);
				if (element == null) {
					result = invocation.proceed();
	
					element = new Element(cacheKey, (Serializable) result);
					
					//annotation没有设expire值则使用ehcache.xml中自定义值
					if (expire > 0) {
						element.setTimeToIdle(expire);
						element.setTimeToLive(expire);
					}
					methodCache.put(element);
				}
			}
		}
		
		return element.getValue();
	}

童子们可以看到invoke方法加了一些判断(比如说类名中是否含有$Proxy),主要是防止越来越多的代理层次,如果被methodcacheinterceptor拦截到的类是一个代理类,那么ahuaxuan暂时还没有找到可以得到该代理类的目标类的方法(望知情者告之,不甚感激).

好了,好像可以告一段落了,因为现在既可以指定缓存某个类所有方法的返回结果,也可以只缓存某个类的某些方法的结果,而且还可以指定某个方法的结果被缓存多长的时间.嗯.
有童子说了:”等等,还有一个需求,我一个类中只有一个方法不需要缓存结果,其他都要缓存结果,怎么办?”
答:别烦了好吗,你就不能自己写一个@MethodNoCache吗,和@ObjectCache联合使用不就解决问题了吗.

文章最后,附上ahuaxuan的源代码,让各位见笑了.
   发表时间:2008-11-07  
如果在过期之前,数据库被别的代码改了,怎么办?
0 请登录后投票
   发表时间:2008-11-07  
除非很多Service都要用类似的缓存读取体系,否则偶不打算使用aop。aop这东西又麻烦,可扩展性又差。可不,为了这method拦截,多了2个annotation,把原来xml里面的东东移到代码里了。

最显著的问题在于,要是一旦某天这段代码被发现存在潜在的bug,那可麻烦了,调试起来可不容易。同时,别人接手这段代码的时候还很困惑,主业务代码里面没啥缓存的代码啊。你要修改这段拦截器的代码,还要小心翼翼的检查Service中的每个方法,是不是会有逻辑上的毛病。
1 请登录后投票
   发表时间:2008-11-07  
aop只有在修改遗留代码的时候才是利器。良好的设计基本上是不需要出现aop的.

可现在明显被滥用了。
0 请登录后投票
   发表时间:2008-11-07   最后修改:2009-03-17
既然兄弟们都说了两句,俺也不能闷着,也凑两句:

不管什么东西,使用它们的时候都有一定的场景,小到组件,再大点到框架,再上升到语言,它们都有各自的优缺点以及适应的场景,项目管理上没有银弹,技术上同样没有,俺们要做的是找到合适的锤子来敲我们手里的钉子.

等哪天俺们哪天不但能从组件或者框架,还能从语言(再从语言到其相应的框架和组件),操作系统,和硬件上来判断我们应该使用哪些,并且能熟练使用+深刻理解其内部原理的时候,俺们就都成架构师了.

Aop cache这把锤子的用途我相信肯定是有的,但是是不是合适大家各自的钉子我就不知道了,所以等要用的时候再拿出来用好了,不过今天先看看这把锤子长什么样也无伤大雅.

引用

如果要系统的讨论缓存问题,请看一系列的文章:
http://www.iteye.com/topic/345693
0 请登录后投票
   发表时间:2008-11-07  
ahuaxuan的帖子一定是要顶的

我们的项目中也是用这种方法做缓存
0 请登录后投票
   发表时间:2008-11-07  
methodCacheInterceptor这个似乎在哪里看过代码
0 请登录后投票
   发表时间:2008-11-07  
写的很好,我以前也有类似的实现,目的都一样,但是还有改进的余地,我是加一个spring tag,<method-cache:annotation-driven />,类似<tx:annotation-driven /> 的原理,不用配那个拦截器,在那个tag的BeanDefinitionParser里,注册一个Advisor和你那个拦截器,那个Advisor可以cut带你那两个annotation的方法或者类,这样不是更爽。
0 请登录后投票
   发表时间:2008-11-07   最后修改:2008-11-07
ray_linn 写道
methodCacheInterceptor这个似乎在哪里看过代码

看页面右上角,或者google一下,到处都是

Norther 写道
写的很好,我以前也有类似的实现,目的都一样,但是还有改进的余地,我是加一个spring tag,<method-cache:annotation-driven />,类似<tx:annotation-driven /> 的原理,不用配那个拦截器,在那个tag的BeanDefinitionParser里,注册一个Advisor和你那个拦截器,那个Advisor可以cut带你那两个annotation的方法或者类,这样不是更爽。

这个主意不错,呵呵
0 请登录后投票
   发表时间:2008-11-07  
Norther 写道
写的很好,我以前也有类似的实现,目的都一样,但是还有改进的余地,我是加一个spring tag,<method-cache:annotation-driven />,类似<tx:annotation-driven /> 的原理,不用配那个拦截器,在那个tag的BeanDefinitionParser里,注册一个Advisor和你那个拦截器,那个Advisor可以cut带你那两个annotation的方法或者类,这样不是更爽。


爽是爽,不过貌似如果这样的话,更加抬高了别人学习的台阶啦。
0 请登录后投票
论坛首页 Java企业应用版

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