注解使程序更加简洁,花更少的力气完成更多的工作——这也是计算机的意义所在。

  • 什么是注解
  • 运行时获取注解信息

1. 注解是什么

Class 对象是 Java 类的说明书,你(通过反射)或者 JVM 阅读该说明书,创建类的实例。
而注解就是说明书中的一小段信息、文本或者标记。

  • 可以携带参数
  • 可以在运行时(@Retention)被阅读

注解本质上也是一个广义的 Java class,也会被编译为 class 文件,创建时选择 Annotation 即可:
image.png

  1. public @interface Target /* extends Annotation */ { // 隐含地继承自 java.lang.annotation.Annotation
  2. // ...
  3. }

2. 元注解

用来修饰注解的注解就是元注解,最常用的就是 @Target@Retention

  • @Target(ElementType)
    • TYPE 【类、接口(包括注解类型)或枚举声明】
    • FIELD 【字段声明(包括枚举常量)】
    • METHOD 【方法声明】
    • PARAMETER 【参数声明】
    • CONSTRUCTOR 【构造方法声明】
    • LOCAL_VARIABLE 【局部变量声明】
    • ANNOTATION_TYPE 【注解类型声明,即声明为元注解,用于注解其他注解】
    • PACKAGE 【包声明】
  • @Retention(RetentionPolicy)注解被保留的策略/等级
    • SOURCE 【只存在于源码中,编译器处理之后被擦除】
    • CLASS(默认) 【被记录到编译后的 .class 文件中,但 VM 运行期中不存在】
    • RUNTIME 【存在于 .class 文件中,而且还能被 JVM 在运行时读入】
  • @Inherited 【定义子类是否可以继承父类定义的注解】
  • @Repeatable 【定义注解是否可以重复使用】

3. 如何定义一个注解

注解的属性可以有:基本数据类型 + String + 类以及他们的数组。

第一步:用@interface 定义注解
第二步:添加参数、默认值(把最常用的参数定义为 value(),推荐所有参数都尽量设置默认值)
第三步:用元注解配置注解(一般是设置 @Target 和 @Retention

  1. // 定义注解
  2. @Retention(RetentionPolicy.RUNTIME)
  3. @Target(ElementType.TYPE)
  4. public @interface Report {
  5. int type() default 0;
  6. String level() default "info";
  7. String value() default "";
  8. }
  9. // 使用注解
  10. @Report(value="即将爆炸", type=2, level="warning")
  11. public class Hello {
  12. }

4. JDK 的常用自带注解

  • @Deprecated 【标记相关元素被废弃】
  • @Override 【标记覆盖/重写】
  • @SuppressWarnings【阻止警告】
  • @FunctionalInterface【标记函数式接口】

5. 注解是如何工作的

定义了注解,并且使用了注解,并不一定生效,最关键的是接下来如何处理注解信息。
原理就是在运行期通过反射获取注解信息,进行动态字节码增强。

  • Method.getAnnotation
  • Class.getAnnotation
  • Field.getAnnotation

可以借助第三方库 Byte Buddy,否则就是自己慢慢通过反射进行操作。

  1. <dependency>
  2. <groupId>net.bytebuddy</groupId>
  3. <artifactId>byte-buddy</artifactId>
  4. <version>LATEST</version>
  5. </dependency>

6. 实战

6.1 编写一个@Log注解来自动生成日志

在运⾏时,拦截方法的进⼊和退出,打印相应的⽇志。

  1. import java.lang.annotation.Retention;
  2. import java.lang.annotation.RetentionPolicy;
  3. @Retention(RetentionPolicy.RUNTIME)
  4. public @interface Log {
  5. String value() default "WARNING!";
  6. int level() default 0;
  7. }
  1. public class MyService {
  2. @Log(value = "ERROR")
  3. public void queryDatabase(int param) {
  4. System.out.println("query db:" + param);
  5. }
  6. @Log
  7. public void provideHttpResponse(String param) {
  8. System.out.println("provide http service:" + param);
  9. }
  10. public void noLog() {
  11. System.out.println("no have log!");
  12. }
  13. }
  1. import net.bytebuddy.ByteBuddy;
  2. import net.bytebuddy.implementation.MethodDelegation;
  3. import net.bytebuddy.implementation.bind.annotation.SuperCall;
  4. import java.lang.reflect.InvocationTargetException;
  5. import java.lang.reflect.Method;
  6. import java.util.List;
  7. import java.util.concurrent.Callable;
  8. import java.util.stream.Collectors;
  9. import java.util.stream.Stream;
  10. public class Main {
  11. // 把 MyService 中所有带有 @Log 注解的方法都过滤出来
  12. static List<String> methodsWithLog = Stream.of(MyService.class.getMethods())
  13. .filter(Main::isAnnotationWithLog)
  14. .map(Method::getName)
  15. .collect(Collectors.toList());
  16. private static MyService enhanceByAnnotation() throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
  17. return new ByteBuddy()
  18. // 创建用于生成子类的 builder
  19. .subclass(MyService.class)
  20. // 控制目标子类上具有哪些方法
  21. .method(method -> methodsWithLog.contains(method.getName()))
  22. // 对匹配到的方法进行拦截,传入定制化的方法实现,继续返回 builder
  23. .intercept(MethodDelegation.to(LoggerInterceptor.class))
  24. // 根据 builder 中的信息生成尚未加载的动态类型(目标子类)
  25. .make()
  26. // 尝试加载该动态类型
  27. .load(Main.class.getClassLoader())
  28. // 获取加载之后的 class 对象
  29. .getLoaded()
  30. .getConstructor()
  31. .newInstance();
  32. }
  33. private static boolean isAnnotationWithLog(Method method) {
  34. // 尝试通过反射获取方法上的注解
  35. return method.getAnnotation(Log.class) != null;
  36. }
  37. // 方法拦截器
  38. public static class LoggerInterceptor {
  39. public static void log(@SuperCall Callable<Void> zuper) throws Exception {
  40. System.out.println("Start!");
  41. try {
  42. zuper.call();
  43. } finally {
  44. System.out.println("End!");
  45. }
  46. }
  47. }
  48. // 测试
  49. public static void main(String[] args) throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException {
  50. MyService service = enhanceByAnnotation();
  51. service.queryDatabase(1);
  52. service.provideHttpResponse("abc");
  53. service.noLog();
  54. }
  55. }

6.2 编写一个 @Cache注解实现缓存AOP(基于注解的缓存装饰器)

装饰器模式(decorator pattern)是一种非常常用的高级设计模式。
请实现一个基于@Cache注解的装饰器,能够将传入的服务类的Class进行装饰,使之具有缓存功能。

如果缓存中不存在方法调⽤的结果,调⽤真实的⽅法,将结果放入缓存。
如果缓存中已经存在结果,检查是否过期。
如果过期,调用真实的⽅法,将结果放入缓存。否则,缓存中存在结果且不过期,直接返回缓存中的结果。

  1. import java.lang.annotation.Retention;
  2. import java.lang.annotation.RetentionPolicy;
  3. @Retention(RetentionPolicy.RUNTIME)
  4. public @interface Cache {
  5. // 标记缓存的时长(秒),默认60s
  6. int cacheSeconds() default 60;
  7. }
  1. import java.util.List;
  2. import java.util.Random;
  3. import java.util.stream.Collectors;
  4. import java.util.stream.IntStream;
  5. public class DataService {
  6. /**
  7. * 根据数据ID查询一列数据,有缓存。
  8. *
  9. * @param id 数据ID
  10. * @return 查询到的数据列表
  11. */
  12. @Cache(cacheSeconds = 2)
  13. public List<Object> queryData(int id) {
  14. // 模拟一个查询操作
  15. Random random = new Random();
  16. int size = random.nextInt(10) + 10;
  17. return IntStream.range(0, size)
  18. .mapToObj(i -> random.nextInt(10))
  19. .collect(Collectors.toList());
  20. }
  21. /**
  22. * 根据数据ID查询一列数据,无缓存。
  23. *
  24. * @param id 数据ID
  25. * @return 查询到的数据列表
  26. */
  27. public List<Object> queryDataWithoutCache(int id) {
  28. // 模拟一个查询操作
  29. Random random = new Random();
  30. int size = random.nextInt(10) + 1;
  31. return IntStream.range(0, size)
  32. .mapToObj(i -> random.nextBoolean())
  33. .collect(Collectors.toList());
  34. }
  35. }
import net.bytebuddy.ByteBuddy;
import net.bytebuddy.implementation.MethodDelegation;
import net.bytebuddy.implementation.bind.annotation.AllArguments;
import net.bytebuddy.implementation.bind.annotation.Origin;
import net.bytebuddy.implementation.bind.annotation.RuntimeType;
import net.bytebuddy.implementation.bind.annotation.SuperCall;
import net.bytebuddy.implementation.bind.annotation.This;
import net.bytebuddy.matcher.ElementMatchers;

import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Objects;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;

public class CacheClassDecorator {
    // 将传入的服务类Class进行增强
    // 使得返回一个具有如下功能的Class:
    // 如果某个方法标注了@Cache注解,则返回值能够被自动缓存注解所指定的时长
    // 这意味着,在短时间内调用同一个服务的同一个@Cache方法两次
    // 它实际上只被调用一次,第二次的结果直接从缓存中获取
    // 注意,缓存的实现需要是线程安全的

    @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(klass.getClassLoader())
                .getLoaded();
    }

    public static class CacheAdvisor {
        private static ConcurrentHashMap<CacheKey, CacheValue> cache = new ConcurrentHashMap<>();

        @RuntimeType
        public static Object cache(
                @SuperCall Callable<Object> zuper,
                @Origin Method method, // 原始方法
                @This Object thisObject, // 当前ByteBuddy动态对象
                @AllArguments Object[] arguments
        ) throws Exception {
            CacheKey cacheKey = new CacheKey(thisObject, method.getName(), arguments);
            final CacheValue resultExistingInCache = cache.get(cacheKey);

            if (resultExistingInCache != null) {
                if (isCacheExpires(resultExistingInCache, method)) {
                    return invokeRealMethodAndPutIntoCache(zuper, cacheKey);
                } else {
                    return resultExistingInCache.value;
                }
            } else {
                return invokeRealMethodAndPutIntoCache(zuper, cacheKey);
            }
        }

        private static boolean isCacheExpires(CacheValue cacheValue, Method method) {
            long time = cacheValue.time;
            int cacheSeconds = method.getAnnotation(Cache.class).cacheSeconds();
            return System.currentTimeMillis() - time > cacheSeconds * 1000;
        }

        private static Object invokeRealMethodAndPutIntoCache(@SuperCall Callable<Object> zuper, CacheKey cacheKey) throws Exception {
            Object realMethodInvocationResult = zuper.call();
            cache.put(cacheKey, new CacheValue(realMethodInvocationResult, System.currentTimeMillis()));
            return realMethodInvocationResult;
        }
    }

    private static class CacheKey {
        private Object thisObject;
        private String methodName;
        private Object[] arguments;

        CacheKey(Object thisObject, String methodName, Object[] arguments) {
            this.thisObject = thisObject;
            this.methodName = methodName;
            this.arguments = arguments;
        }

        @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) &&
                    Objects.equals(methodName, cacheKey.methodName) &&
                    Arrays.equals(arguments, cacheKey.arguments);
        }

        @Override
        public int hashCode() {
            int result = Objects.hash(thisObject, methodName);
            result = 31 * result + Arrays.hashCode(arguments);
            return result;
        }
    }

    private static class CacheValue {
        private final Object value;
        private final long time;

        CacheValue(Object value, long time) {
            this.value = value;
            this.time = time;
        }
    }

    public static void main(String[] args) throws Exception {
        DataService dataService = decorate(DataService.class).getConstructor().newInstance();

        // 有缓存的查询:只有第一次执行了真正的查询操作,第二次从缓存中获取
        System.out.println(dataService.queryData(1));
        Thread.sleep(1 * 1000);
        System.out.println(dataService.queryData(1));
        Thread.sleep(2 * 1000);
        System.out.println(dataService.queryData(1));

        // 无缓存的查询:两次都执行了真正的查询操作
        System.out.println(dataService.queryDataWithoutCache(1));
        Thread.sleep(1 * 1000);
        System.out.println(dataService.queryDataWithoutCache(1));
    }
}

7. 参考

  1. 自定义注解详细介绍
  2. 注解-廖雪峰