Bootstrap

java:aocache:基于aspectJ实现的方法缓存工具

背景

最近一直在做一些服务端的设计,经常遇到常量计算的问题,比如获取查找一个类的所有方法,获取有指定注解(Annnotation)的方法并查找注解的上特定的元注解是否有特定的值 。。。。总之逻辑很复杂,而且会频繁调用。
比如在服务端方法拦截器上经常执行这样的计算,事实上在运行时,对于一个类(Class)或方法(Method),它在运行时就是个常量,上述复杂计算返回结果也是一个恒定的值不会改变,称之为常量计算。
如果每次请求同一个服务方法都要重复执行这样的计算,无疑是对性能的浪费。

FunctionCached

为此,我很久之前就实现了一个解决个问题的工具类FunctionCached

完整代码参见 :https://gitee.com/l0km/common-java/blob/master/common-base2/src/main/java/net/gdface/cache/FunctionCached.java
相关说明参见之前的博客:《java:基于guava缓存(LoadingCache)实现结果缓存避免重复计算》
《java:基于弱引用(WeakReference)的FunctionCached实现》

有了FunctionCached,实现一个方法的缓存就比较方便了,大概是如下的样子:

	/** 缓存对象 */
	private final FunctionCached<String, Class<?>> CACHED_CLASS_FORNAME = 
		FunctionCached.builder()
		/** 弱引用值模式 */
		.weakValues()
		.nullable().getterFunction(
			new Function<String, Class<?>>() {
			@Override
			public Class<?> apply(String suffix) {
				/** 执行计算 */
				return classFormName0(suffix);
			}
		}).build();
	/** 真正负责计算的方法 */
	private Class<?> classFormName0(String className){
		// DO SOMETHING
	}
	/** 
	 * 对外提供的计算方法,
	 * 方法实现就是从缓存获取参数为key的值,
	 * 如果值不存在缓存自动执行 classFormName0,将结果再存入缓存,避免下次再计算。 
	 */
	public Class<?> classFormName(String className){
		return CACHED_CLASS_FORNAME.get(className);
	}

如上的模式大概就是一内一外两个方法,内部方法负责真正的计算由缓存对象调用,外部方法则直接从缓存读取数据提供给调用方。

烦恼

话说这个FunctionCached我用了好长时间,一直觉得还不错。上个月写了一个新项目casban,项目中用FunctionCached写了十几个不同的缓存方法,写得我不胜其烦。
两周前才结束的项目beanfilter又涉及到大量的方法缓存。
这境遇,让我深感在大量需要计算结果缓存提高性能的场景下,FunctionCached用起来还是不顺手。要是能更简单就好了。

不满的种子开始发芽----必须要做点什么改变现状。

设计目标

为了彻底解决这个麻烦,前阵子断断续续的开始构思一个新的工具,上周开始设计,目标设计是一个更方便使用且通用的方法(Method)缓存工具,如果也能实现单实例(Constructor)缓存则更好。
项目定名aocache(Aspect Oriented Cache),定下了如下的设计目标:

  • 支持 1.7及以上JDK版本

    这样才有更广泛的通用性,相对底层的通用工具设计时也能用得上。

  • 依赖库少

    最好没有依赖库,目的同上。

  • 方便使用

    以注解(Annotaion)标记为使用主要方式。

  • 支持对不同方法个性化配置缓存,

    比如支持弱引用模式(WeakReference),软引用模式(SoftReference)以减少不必要的资源占用。

效果

经过一周的设计,总算完成,发布第一个版本0.2.0,达到预期设计目标。以前面的classFormName方法的例子来对比的话,最终的使用效果如果就是这样:

	/** 注解定义该方法由aocache提供结果缓存 */
	@AoCacheable
	public Class<?> classFormName(String className){
		// DO SOMETHING
	}

是不是很简单?十几行代码缩减为一个注解@AoCacheable

aocache

现在开始正式介绍 aocache:

aocache(Aspect Oriented Cache)是一个基于aspectJ实现的方法(Method/Constructor)缓存工具。

设计用于对方法计算返回的结果缓存,以实现同的输入参数只计算一次,第二次以后调用法则返回第一次计算的结果,以减少重复计算提高系统运行效率,适用于运行时(runtime)输入参数与输出结果保持恒定映射的场景。

aocache基于切面编程(Aspect-Oriented Programming)框架实现方法调用拦截,保存计算结果的缓存基于guavaLoadingCache缓存实现。

快速入门

pom.xml配置

引入依赖

			<dependency>
				<groupId>com.gitee.l0km</groupId>
				<artifactId>aocache</artifactId>
				<version>0.4.0</version>
			</dependency>

aocache不会传递依赖任何其他依赖库

增加maven插件aspectj-maven-plugin用于项目编译时织入(Compile Time Weaving)。

Compile Time Weaving:
编译时织入也叫静态织入,是aspectj最简单的织入形式。aspectj-maven-plugin插件执行aspectj编译器AJC 在源码编译过程中将切面拦截代码添加到Java编译器生成的的.class。

	<build>
		<plugins>
            <plugin>
				<groupId>org.codehaus.mojo</groupId>
				<artifactId>aspectj-maven-plugin</artifactId>
				<version>1.10</version>
				<configuration>
					<source>1.7</source>
					<target>1.7</target>
					<encoding>UTF-8</encoding>
					<complianceLevel>1.7</complianceLevel>
					<verbose>true</verbose>
					<showWeaveInfo>true</showWeaveInfo>
					<aspectLibraries>
						<aspectLibrary>
							<groupId>com.gitee.l0km</groupId>
							<artifactId>aocache</artifactId>
						</aspectLibrary>
					</aspectLibraries>
				</configuration>
				<executions>
					<execution>
						<goals>
							<goal>compile</goal>
							<goal>test-compile</goal>
						</goals>
					</execution>
				</executions>
			</plugin>
		</plugins>
	</build>

源码注解

如下在AocacheTest.hello(String)方法上增加注解@AoCacheable就激活了该方法的aocache缓存能力。

如果IDE环境没有配置aspectj的合适环境,下面的单元测试可能无法在IDE中正确运行。我的eclipse目前还不行。

示例如下:

AocacheTest.java

import static org.junit.Assert.*;

import org.junit.Test;

import com.gitee.l0km.aocache.MemberCache;
import com.gitee.l0km.aocache.annotations.AoCacheable;

public class AocacheTest {
	@AoCacheable
	private String hello(String name) {
		return "hello,"+String.valueOf(name);
	}

	@Test
	public void test() {
		try {
			String s = hello("jerry");
			System.out.println(s);
			/** 第二次以后调用返回结果与第一次是同一个对象 */
			System.out.printf("%s %b\n",hello("jerry"),hello("jerry")==s);
			System.out.printf("%s %b\n",hello("jerry"),hello("jerry")==s);
			System.out.printf("%s %b\n",hello("jerry"),hello("jerry")==s);
			System.out.printf("%s %b\n",hello("jerry"),hello("jerry")==s);
			/** 获取方法实际的的执行次数 */
            long hitCount = MemberCache.INSTANCE.invokeCountOf(this, "hello",new Class<?>[]{String.class},new Object[]{"jerry"});
           System.out.printf("调用次数 %d\n", hitCount);
            /** 断言:hello(String)方法对于参数'jerry'的调用只有一次 */
			assertEquals(1, hitCount);
		} catch (Throwable e) {
			e.printStackTrace();
			fail(e.getMessage());
		}
	}

}

编译

mvn clean install

maven插件aspectj-maven-plugin输出显示hello方法已经被增加了拦截点(Join Point)

[INFO] --- aspectj-maven-plugin:1.10:test-compile (default) @ aocache-example-ctw ---
[INFO] Showing AJC message detail for messages of types: [error, warning, fail]
[INFO] Join point 'method-execution(void com.gitee.l0km.aocache.example.ctw.AocacheCtwTest.hello())' in Type 'com.gitee.l0km.aocache.example.ctw.AocacheCtwTest' (AocacheCtwTest.java:24) advised by around advice from 'com.gitee.l0km.aocache.aop.AocacheAnnotatedAspect' (aocache-0.0.0-SNAPSHOT.jar!AocacheAnnotatedAspect.class(from AocacheAnnotatedAspect.java))

执行单元测试:

mvn -Dtest=AocacheTest -DskipTests=false test

输出:

 T E S T S
-------------------------------------------------------
Running com.gitee.l0km.aocache.example.ctw.AocacheTest
hello,jerry
hello,jerry
hello,jerry
hello,jerry
hello,jerry
调用次数 1
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.084 sec

Results :

Tests run: 1, Failures: 0, Errors: 0, Skipped: 0

@AoCacheable

aocache为每个方法定义一个独立的缓存对象。aocache允许为每个方法配置不同的缓存策略, 最简单的方法就是通过@AoCacheable注解实现缓存配置

@AoCacheable注解定义在方法指定缓存该方法的的返回值。通过@AoCacheable上定义的字段,可以精确配置每个方法缓存。

@AoCacheable 也可以定义在构造方法上,定义在构造方法就为该类实现了单实例缓存

字段名默认值说明
debugOutputfalse是否输出调试信息
debugOutputDetailfalse是否输出详细调试信息
weakKeysfalse指定存储在缓存中的每个键(而不是值)都应封装在WeakReference中(默认情况下,使用强引用)。
weakValuesfalse指定存储在缓存中的每个值(而不是键)都应包装在SoftReference中(默认情况下,使用强引用)。软引用的对象将以全局最近最少使用的方式进行垃圾收集,以响应内存需求。
softValuesfalse指定存储在缓存中的每个值(而不是键)都应包装在SoftReference中(默认情况下,使用强引用)。软引用的对象将以全局最近最少使用的方式进行垃圾收集,以响应内存需求。
initialCapacity-1设置内部哈希表的最小总大小。例如,如果初始容量为60,并发级别为8,则创建八个段,每个段都有一个大小为8的哈希表。在构建时提供足够大的估计值可以避免以后进行昂贵的调整大小操作,但不必设置此值—过高会浪费内存。
concurrencyLevel0指定更新操作之间允许的并发性。用作内部大小调整的提示。该表在内部进行了分区,以尝试在没有争用的情况下允许指定数量的并发更新。
maximumSize-1指定缓存可以包含的最大条目数。请注意,缓存可能会在超过此限制之前收回一个条目。随着缓存大小接近最大值,缓存会收回不太可能再次使用的条目。例如,缓存可能会因为最近或不经常使用而被逐出。
maximumWeight-1指定缓存中可能包含的项的最大权重。重量是使用地磅指定的秤来确定的,使用这种方法需要在调用构建之前对地磅进行响应调用。
expireAfterWrite-1指定在创建条目或最近替换其值后经过固定的持续时间后,应自动从缓存中删除每个条目。
expireAfterWriteTimeUnitMINUTES(分钟)expireAfterWrite的时间单位
expireAfterAccess-1指定在创建条目、最近一次替换其值或最后一次访问之后经过固定的持续时间后,应自动从缓存中删除每个条目。访问时间由所有缓存读取和写入操作(包括cache.asMap().get(Object)和cache.asMop().put(K,V))重置,但不由cache.asMap的集合视图上的操作重置。
expireAfterAccessTimeUnitMINUTES(分钟)expireAfterAccess的时间单位
refreshAfterWrite-1指定活动条目的创建或其值的最新替换后经过固定的持续时间后,活动条目的自动刷新条件。刷新的语义在LoadingCache.refresh中指定,并通过调用CacheLoader.reload来执行。
由于CacheLoader.reload的默认实现是同步的,建议此方法的用户使用异步实现覆盖CacheLoader.reload;否则将在不相关的缓存读取和写入操作期间执行刷新。
当前自动刷新是在对条目的第一个过时请求发生时执行的。触发刷新的请求将对CacheLoader.reload进行阻塞调用,如果返回的future完成,则立即返回新值,否则返回旧值。
refreshAfterWriteTimeUnitMINUTES(分钟)refreshAfterWrite的时间单位

弱引用模式

默认配置使用强引用模式保存Key和Value,这种情况下,计算结果一旦产生常驻内存。

弱引用模式允许在JVM自动清理不再被引用的计算结果(VALUE)或其对应的KEY。下次调用时再重新计算。适用于不需要长期保存的数据。

所以用户可以根据自己的场景选择使用强引用还是弱引用配置缓存。

@Cacheable Of Spring

spring也使用 @Cacheable注解也提供了类似的方法缓存功能,那么aocache与之有什么不同?
显然,spring的方法缓存功能只能用于spring环境下,而aocache只要求JDK 1.7以上任何Java开发环境。

项目地址

码云仓库位置:https://gitee.com/l0km/aocache
请访问git仓库获取完整说明

示例项目

aocach-example 为aocache的示例项目,
项目中实践了aocache在aspectJ的三种织入模式(CTW/LTW/PCW)下的应用。
码云仓库位置:https://gitee.com/l0km/aocache-example

参考资料

《Chapter 5. Load-Time Weaving》
《The AspectJTM 5 Development Kit Developer’s Notebook》

;