什么是注解呢?还记得Class是什么吗?注解就是Class对象中的⼀⼩段信息/⽂本/标记,它可以携带参数,可以在运⾏时被阅读。这就是注解的全部了。怎么处理注解并不是注解要关注的信息,它只是这么一小段信息。可用如下代码新建一个注解。
public @interface MyAnnotation{}
JDK自带了一些注解:
-
@Deprecated
一些废弃的方法可以写上这个注解,用的时候会用中划线提示已被废弃。 -
@Override
重写的注解,其实不写也完全没有问题,他可以保证我们不会写错方法名。 -
@SuppressWarnings
用于压制一些警告。 -
@FunctionalInterface
告诉我们哪些是函数接口,当然没有这个也是可以工作的。
元注解
元注解就是可以放在注解上的注解,包括有:
@Rentention
/*
RetentionPolicy.CLASS 会在字节码中保留,运行时不会保留,这是默认的。
RetentionPolicy.SOURCE 只出现在源代码中,不会被保留。
RetentionPolicy.RUNTIME 会在字节码中保留,运行时会保留
*/
@Rentention(RetentionPolicy.CLASS)
public @interface MyAnnotation{}
-
@Target
指定注解可以放在哪里。
@Target({ElementType.METHOD, ElementType.CONSTRUCTOR})
public @interface MyAnnotation{}
-
@Documented
它会被 javadoc 之类的工具处理 -
@Inherited
是否能被子类看到,很少用 -
@Repeatable
重复,很少用
注解的属性
注解的属性可以有基本数据类型、String、类对象以及它们的数组。且可以有默认值,有如下的一个例子:
public @interface 洗涤手段 {
String[] value() default "干洗";
String 成分();
}
@洗涤手段(value = {"干洗","水洗"}, 成分="纯棉")
public class 毛衣 {}
一个例子说明注解是如何工作的
注解仅仅是⼀段信息,它⾃⼰⽆法⼯作,换句话说,没有东⻄处理它们的话,注解没有任何卵⽤。这里写一个简单的例子来说明如何让注解工作。我们编写一个@Log
注解来自动生成日志。有这样一个需求,我们需要在查询数据库和设置数据时的前后都加入一段日志。其中一个办法就是用注解来实现。
public class MyService {
public void getDataFromDatabase() {
System.out.println("get data");
}
public void setData() {
System.out.println("set data");
}
}
首先先来介绍以下动态字节码增强,动态字节码增强就是在运行时在内存中凭空根据旧类增强出一个新类,使得旧类有类更强的能力。拿上面的例子来说,原本只是MyService
类,在运行时我们使用动态字节码增强,搞出一个新类MyServiceSub
,这个类根据MyService
中的@Log
注解,实现了新的功能(打印日志)。要完成动态字节码增强,我们可以使用ByteBuddy。找到官网的教程抄就完事了:
class LoggerInterceptor {
public static List<String> log(@SuperCall Callable<List<String>> zuper)
throws Exception {
System.out.println("Calling database");
try {
return zuper.call();
} finally {
System.out.println("Returned from database");
}
}
}
MemoryDatabase loggingDatabase = new ByteBuddy()
.subclass(MemoryDatabase.class)
.method(named("load"))
.intercept(MethodDelegation.to(LoggerInterceptor.class))
.make()
.load(getClass().getClassLoader())
.getLoaded()
.newInstance();
首先我们定义注解,值得注意的是@Retention
的属性一定要是RUNTIME
,因为我们是要在运行时根据注解进行动态字节码增强。
@Retention(RetentionPolicy.RUNTIME)
public @interface Log {}
给MyService
增加注解和一个不带注解方法,加以区分。 methodsWithLogAnnotation
储存了带有@Log
注解的方法名
public class MyService {
public static final List<String> methodsWithLogAnnotation = Arrays.stream(MyService.class.getMethods())
.filter(MyService::isAnnotationWithLog)
.map(Method::getName)
.collect(Collectors.toList());
private static boolean isAnnotationWithLog(Method method) {
return method.getAnnotation(Log.class) != null;
}
@Log
public void getDataFromDatabase() {
System.out.println("get data");
}
@Log
public void setData() {
System.out.println("set data");
}
public void methodWithNoLogAnnotation() {
System.out.println("methodWithNoLogAnnotation");
}
}
主程序中,我们使用ByteBuddy动态增强字节码。其实一看ByteBuddy的结构就可以推断它是一种Builder模式。
其中method
方法接受ElementMatcher<? super MethodDescription> matcher
这样一个参数,而ElementMatcher
是一个接口,我们直接实现它就好了,筛选一些需要被处理的方法。FilterMethodWithLogAnnotation
是我们自己实现好的类。值得注意的是ElementMatcher
的泛型需要是MethodDescription
或是其父类。
intercept
方法用于拦截方法,并指定拦截的方法要做的事情。通常有两种形式:
.intercept(FixedValue.value("Hello World ByteBuddy!"))
.intercept(MethodDelegation.to(LoggerInterceptor.class))
load
方法将新类加载到JVM中,需要传入一个ClassLoader
通常有这样写法,由于我们使用的是静态方法,所以改成了Main.class
。
.load(getClass().getClassLoader())
.load(Main.class.getClassLoader())
以下是完整的实现:
public class Main {
public static void main(String[] args) throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException {
MyService service = enhanceByAnnotation();
service.getDataFromDatabase();
service.setData();
service.methodWithNoLogAnnotation();
}
private static MyService enhanceByAnnotation() throws IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
return new ByteBuddy()
.subclass(MyService.class)
.method(new FilterMethodWithLogAnnotation())
.intercept(MethodDelegation.to(LoggerInterceptor.class))
.make()
.load(Main.class.getClassLoader())
.getLoaded()
.getConstructor()
.newInstance();
}
public static class LoggerInterceptor {
public static void log(@SuperCall Callable<Void> zuper)
throws Exception {
System.out.println("start");
try {
zuper.call();
} finally {
System.out.println("end");
}
}
}
private static class FilterMethodWithLogAnnotation implements ElementMatcher<MethodDescription> {
@Override
public boolean matches(MethodDescription target) {
return MyService.methodsWithLogAnnotation.contains(target.getName());
}
}
}
其实不需要.method(new FilterMethodWithLogAnnotation())
这么麻烦,ByteBuddy为我们提供了ElementMatchers
一系列匹配的方法,我们可以用.method(ElementMatchers.isAnnotatedWith(Log.class))
替换掉上面一堆啰嗦的代码。
练习:编写@Cache注解实现缓存AOP
搭建整体框架
定义Cache注解
@Retention(RetentionPolicy.RUNTIME)
public @interface Cache {
}
定义Cache装饰器,CacheDecorator
的作用就是动态生成一个子类,增强带有Cache注解的方法。其中在intercept方法中我们采用了一些ByteBuddy自带的注解,注解的解释如下:
-
@RuntimeType
当拦截方法发生的时候,能够找到cache拦截方法去调用它。 -
@SuperCall
注释可以从动态类外部实现执行父类方法的调用。用人话来说就是调用父类方法(那个真实的方法)。 -
@AllArguments
注解拿到当前的调用方法的参数数组。 -
@This
拿到当前的实例对象。 - 使用
@Origin
注解的参数必须为Method
,Constructor
,Executable
,Class
,MethodHandle
,MethodType
,String
和int
类型中的任何类型中的一个。根据参数的类型,会为它分配一个方法或构造方法引用,该方法或构造方法对现在已检测到的原始方法或构造方法的引用,或对动态创建的类的引用。用人话来说如果是@Origin Method method
,则会拿到当前调用的方法。
public class CacheDecorator {
@SuppressWarnings("unchecked")
public static <T> Class<T> decorate(Class<T> klass) {
return (Class<T>) new ByteBuddy()
.subclass(klass)
.method(ElementMatchers.isAnnotatedWith(Cache.class))
.intercept(MethodDelegation.to(CacheAdvisor.class))
.make()
.load(CacheDecorator.class.getClassLoader())
.getLoaded();
}
public static class CacheAdvisor {
@RuntimeType
public static Object cache(
@SuperCall Callable<Object> zuper,
@Origin Method method,
@This Object thisObject,
@AllArguments Object[] arguments
) {
System.out.println(method);
System.out.println(thisObject);
System.out.println(Arrays.toString(arguments));
return null;
}
}
}
定义一个带缓存的方法,和一个不带缓存的方法做比较,在psvm
中我们使用CacheDecorator
增强DataService
类,使其带有缓存的处理功能。
public class DataService {
@Cache
public List<Object> queryData(int id) {
Random random = new Random();
int size = random.nextInt(10) + 10;
return IntStream.range(0, size)
.mapToObj(i -> random.nextInt(10))
.collect(Collectors.toList());
}
public List<Object> queryDataWithoutCache(int id) {
Random random = new Random();
int size = random.nextInt(10) + 10;
return IntStream.range(0, size)
.mapToObj(i -> random.nextInt(10))
.collect(Collectors.toList());
}
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, InterruptedException {
DataService service = CacheDecorator.decorate(DataService.class).getConstructor().newInstance();
System.out.println(service.queryData(1));
Thread.sleep(1000);
System.out.println(service.queryData(1));
System.out.println(service.queryDataWithoutCache(1));
Thread.sleep(1000);
System.out.println(service.queryDataWithoutCache(1));
}
}
这时我们也可以运行一下看下结果,确保我们增强了动态增强了DataService,获取方法和获取参数也都能够正常使用。
public java.util.List com.github.lazyben.DataService.queryData(int)
com.github.lazyben.DataService$ByteBuddy$slKdwzx2@4e41089d
[1]
// .....
完成缓存拦截的逻辑
我们需要定义一个Map<CacheKey,Object>
存放方法调用的值,用于实现缓存,其中CacheKey
包含参数arguments
,调用的对象thisObject
,和方法名字methodName
,如果这三个值全都对的上才能说明CacheKey
已经在缓存中存在(真实的方法已经调用过了),我们才去缓存中取。值得注意的是,我们必须给CacheKey
类重写equals
和hashCode
方法。
public class CacheDecorator {
// ......
private static class CacheKey {
private final Object thisObject;
private final Object[] arguments;
private final String methodName;
public CacheKey(Object thisObject, Object[] arguments, String methodName) {
this.thisObject = thisObject;
this.arguments = arguments;
this.methodName = methodName;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
CacheKey cacheKey = (CacheKey) o;
return Objects.equals(thisObject, cacheKey.thisObject) &&
Arrays.equals(arguments, cacheKey.arguments) &&
Objects.equals(methodName, cacheKey.methodName);
}
@Override
public int hashCode() {
int result = Objects.hash(thisObject, methodName);
result = 31 * result + Arrays.hashCode(arguments);
return result;
}
}
public static class CacheAdvisor {
private static final ConcurrentHashMap<CacheKey, Object> cache = new ConcurrentHashMap<>();
@RuntimeType
public static Object cache(
@SuperCall Callable<Object> zuper,
@Origin Method method,
@This Object thisObject,
@AllArguments Object[] arguments
) throws Exception {
final CacheKey cacheKey = new CacheKey(thisObject, arguments, method.getName());
final Object resultExistingCache = cache.get(cacheKey);
if (resultExistingCache != null) {
return resultExistingCache;
} else {
Object realMethodResult = zuper.call();
cache.put(cacheKey, realMethodResult);
return realMethodResult;
}
}
}
}
来看一下结果,上面两个数组完全相同,说明第二个数组是从缓存中拿出来的,下面两个则反之。我们已经成功实现了缓存功能。
[7, 1, 4, 2, 0, 5, 7, 6, 2, 6, 9, 8, 0, 7]
[7, 1, 4, 2, 0, 5, 7, 6, 2, 6, 9, 8, 0, 7]
[2, 6, 6, 8, 6, 1, 7, 5, 3, 3]
[2, 8, 7, 8, 7, 5, 4, 4, 3, 2, 6, 8, 7]
加入缓存时间过期机制
给注解加上cacheSecond
属性,默认值为60秒。
@Retention(RetentionPolicy.RUNTIME)
public @interface Cache {
int cacheSecond() default 60;
}
将原来的Map<CacheKey, Object>
变为Map<CacheKey, CacheValue>
,其中CacheValue
含有两个成员变量value
表示结果,time
则是储存的时间。
public class CacheDecorator {
@SuppressWarnings("unchecked")
public static <T> Class<T> decorate(Class<T> klass) {
//......
}
private static class CacheKey {
//......
}
private static class CacheValue {
public Object value;
public long time;
public CacheValue(Object value, long time) {
this.value = value;
this.time = time;
}
}
public static class CacheAdvisor {
private static final ConcurrentHashMap<CacheKey, CacheValue> cache = new ConcurrentHashMap<>();
@RuntimeType
public static Object cache(
@SuperCall Callable<Object> zuper,
@Origin Method method,
@This Object thisObject,
@AllArguments Object[] arguments
) throws Exception {
final CacheKey cacheKey = new CacheKey(thisObject, arguments, method.getName());
final CacheValue cacheValue = cache.get(cacheKey);
if (cacheValue != null) {
if (isExpired(cacheValue.time, method.getAnnotation(Cache.class).cacheSecond())) {
return invokeRealMethodAndPutItIntoMap(zuper, cacheKey);
} else {
return cacheValue.value;
}
} else {
return invokeRealMethodAndPutItIntoMap(zuper, cacheKey);
}
}
private static Object invokeRealMethodAndPutItIntoMap(@SuperCall Callable<Object> zuper, CacheKey cacheKey) throws Exception {
Object realMethodResult = zuper.call();
cache.put(cacheKey, new CacheValue(realMethodResult, System.currentTimeMillis()));
return realMethodResult;
}
private static boolean isExpired(long time, int cacheSecond) {
return System.currentTimeMillis() - time > cacheSecond * 1000;
}
}
}
最后附上逻辑流程图
网友评论