美文网首页
Java 注解

Java 注解

作者: BitterOutsider | 来源:发表于2020-12-09 19:37 被阅读0次

什么是注解呢?还记得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, Stringint 类型中的任何类型中的一个。根据参数的类型,会为它分配一个方法或构造方法引用,该方法或构造方法对现在已检测到的原始方法或构造方法的引用,或对动态创建的类的引用。用人话来说如果是@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类重写equalshashCode方法。

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

最后附上逻辑流程图


相关文章

  • 菜鸟学服务端技术----Spirng基础

    注解 Java基础加强总结(一)——注解(Annotation) java中的注解是如何工作的? java 注解 ...

  • 自定义注解

    java annotation基础 java注解分为标准注解和元注解。 标准注解是java为我们提供的预定义的注解...

  • Java注解的使用

    Java注解的使用 参考 廖雪峰java教程 使用注解 什么是注解(Annotation)?注解是放在Java源码...

  • Java原生注解和Spring注解的说明

    注解 java 原生注解 Spring 中的注解 一 Java原生注解 Java注解是在JDK1.5以后引入的新特...

  • Java 注解

    JAVA注解 Java 自带注解(系统注解) @Override 表示重写注解 @Deprecated 表示过时的...

  • 1.8 Java 注解annotation

    1.1 注解声明 Java注解Annotation,有声明注解和元注解 元注解:Java提供的元注解,所谓元注解就...

  • Java注解学习总结(脑图)

    注解的提取测试:定义注解: 测试注解提取: 参考:《Java编程思想》java注解

  • Java注解

    Java注解(Annotation)详解(一)——概述及JDK自带注解 Java注解(Annotation)详解(...

  • JAVA-注解 Annotation

    JAVA-注解 Annotation sschrodinger 2018/6/4 基本 注解 Java 注解用于为...

  • Java注解简介篇

    摘要 本文详细介绍java注解是什么,如何声明java注解,如何解析java注解。最后介绍JDK提供的几大基本注解...

网友评论

      本文标题:Java 注解

      本文链接:https://www.haomeiwen.com/subject/jsazwktx.html