Java的注解

822 阅读10分钟

Java的注解(annotation)

\quad最早接触注解的时候,还是在继承那里讲到覆盖父类的方法的时候,子类覆盖了父类的方法的时候:

@Override
    public int hashCode() {
        return Objects.hash(XXX);
    }

\quad这个@Override注解用以标识这个方法覆盖了父类的方法,其实把注解去掉也没事。那么问题就来了,注解在代码中到底是怎么样一种存在,既然可有可无,为什么还用途这么广呢?
\quad 引用《Java编程思想》中的话来定义注解的话,就是注解(也被称为元数据)为我们在代码中添加信息提供了一种形式化的方法,使我们在稍后某个时刻非常方便地使用这些数据。其实说到底,注解就是类中的一小段信息,可以携带参数,可以在运行时被阅读。但是要知道,这小段信息编译器是不会去识别并且解析的,只是为我们去实现自己的逻辑所服务的。

1.注解的基本语法

\quad 前面提到的@Override是Java SE5内置注解中的其中一种,在这先详细介绍这三种内置注解:

  • @Override,表示当前的方法定义为将覆盖超类的方法。如果不小心拼写错误,或者方法签名对不上被覆盖的方法,编译器就会发出警告信息。
  • @Deprecated,表示被注解的内容已经废弃,如果程序员使用了注解为它的元素,那么编译器会发出警告信息。
  • @SuppressWarnings,关闭不当的编译器警告信息。

\quad 那么如何新建一个符合我们需求的注解呢?与新建类一样:

语法如下:

public @interface SpringBootApplication {
}

注意:注解都隐含的继承了Annotation类。
点击运行的话,可以看到在target目录下,为注解也生成了一个临时文件

这时候就可以把这个注解加到任何地方了:


@SpringBootApplication
public class MyStringHashMapDecorator {
    @SpringBootApplication
    HashMap hashMap = new HashMap();

    @SpringBootApplication
    public void put(String key, String value) {
        hashMap.put(key, value);
    }

2.元注解

\quad但有时候我们必须限制注解能够标记的位置,或者想保留注解到运行期间,这时候就需要元注解(标记注解的注解)了,Java目前只内置了五种元注解:

注解
含义
@Target
(默认情况下是可以标记在任何元素上)
表示该注解可以用于什么地方。可选范围包括:
CONSTRUCTOR:构造器的声明
FIELD:域声明(包括enum)
LOCAL_VARIABLE:局部变量声明
METHOD:方法声明
PACKAGE:包声明
PARAMETER:参数声明
TYPE:类、接口(包括注解类型)或者enum枚举声明
ANNOTATION_TYPE:标记注解的注解
在JDK1.8之后,新添了两个:
TYPE_USE:类型使用声明
TYPE_PARAMETER:类型参数声明
@Retention
(默认为CLASS级别)
表示需要在什么级别保存该注解信息。
可选的RetentionPolicy参数包括:
SOURCE:注解将会被编译器丢弃
CLASS:注解在class文件中可用,但会被VM丢弃
RUNTIME:VM将在运行期也保留注解,因此可以通过反射机制读取注解的信息
@Document 将此注解包含在Javadoc中
@Inherited 允许子类继承父类的注解
@Repeatable 允许注解重复

下面就逐一用例子演示一下上面的声明:
  • 1.ElementType.Constructor-适用于构造器
public class MyStringHashMapDecorator {
    
    String name;
    
    @SpringBootApplication
    public MyStringHashMapDecorator(String name) {
        this.name = name;
    }

  • 2.ElementType.Field(这个Field最好还是做成员变量讲)
    @SpringBootApplication
    public static final int number=1;
    @SpringBootApplication
    String name;
    @SpringBootApplication
    HashMap hashMap = new HashMap();

    enum spell{
        @SpringBootApplication
        a,
        b,
        c,
        d
    }
  • 3.ElementType.LOCAL_VARIABLE-适用于局部变量
public void put(String key, String value) {
        @SpringBootApplication
        int i=0;
    }
  • 4.ElementType.METHOD-适用于方法
@SpringBootApplication
    public void put(String key, String value) {
        
    }
  • 5.ElementType.Package-适用于包声明
    其实在每个包里面都可以放一个package-info

可以在其中添加注解:

@SpringBootApplication
package com.github.hcsp.test;
  • 6.ElementType.PARAMETER-适用于参数声明
public void put(@SpringBootApplication String key, String value) {

    }
  • 7.ElementType.TYPE-适用于类,接口或enum声明
    \quad类、接口没什么好说的,就是enum声明,前面在FIELD中不是提到过嘛?其实不然,这里的eunm是对整个枚举类型而言的,而FIELD那里只是对单个实例而言的。
@SpringBootApplication
    enum spell{

        a,
        b,
        c,
        d
    }
  • 8.ANNOTATION_TYPE-标记注解
@SpringBootApplication
public @interface SpringBootApplications {
    
}
  • 9.TYPE_USE-类型使用声明
    可以标注任何类型名称。(不能作用于包声明及方法声明)
  • 10.TYPE_PARAMETER-对泛型的声明
public class MyStringHashMapDecorator<@SpringBootApplication T>
  • @Repeatable用法

一般情况下,重复声明注解都是会报错的:

所有这时候就要体现@Repeatable的价值了:
代码如下

public @interface SpringBootApplications {
    SpringBootApplication[] value();
}

@Repeatable(SpringBootApplications.class)
public @interface SpringBootApplication {
    
}

这时候就不报错了。

3.注解元素

在注解中同样可以声明成员变量。但是对成员变量的类型有要求:

  • 所有的基本类型
  • String类型
  • Class
  • enum
  • Annotation

注意:不能使用Integer等装箱类型,也不能使用Object。
同时,还可以给注解元素设置默认值:

public @interface SpringBootApplication {
    public int id() default 0;
    public String klass() default " ";
}

但是注意:默认值不能为null,换句话说就是不能有不确定的值。

4.注解实用

如果不人为对注解进行处理,注解就类似于注释了。所以,下面用两个例子来介绍注解在日常中的用处:

  • 1.对标记为@Log的方法实现动态字节码增强
    假设我们现在有这样两个方法:
public class AnnotationPractise {
    private static void queryDatabase(){
        System.out.println("queryDatabase...");
    }
    private static void insertDatabase(){
        System.out.println("insertDatabase...");
    }

    public static void main(String[] args) {
        queryDatabase();
        selectDatabase();
    }
}

\quad查询数据库和往数据库中插入数值的操作,现在我们想每次操作开始前,都显示一下当时的时间,比如这样:

14:51:58.210
queryDatabase...
14:51:58.210
14:51:58.211
selectDatabase...
14:51:58.211

\quad这并不是什么难事,在前后加上LocalTime.now即可,但是问题来了,如果我有成百上千个方法都要这样做呢,兴许某一天我不想要时间了,而是加一些别的话呢,没有人会去一个一个改吧?所以这时候就可以用上注解了。
\quad实现思路在于,新建一个Log注解,为每个需要添加额外信息的方法添加Log注解,接着在主函数中利用反射机制,拿到类中的所有方法,过滤出其中带有Log标记的方法,使用动态字节码增强技术,实现功能的扩展。

  • 新建一个Log注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)//保留到运行期间,否则不能通过反射拿到方法
public @interface Log {
}
  • 为需要增强的方法添加注解
public class AnnotationPractise {
    @Log
    public void queryDatabase() {
        System.out.println("queryDatabase...");
    }

    @Log
    public void selectDatabase() {
        System.out.println("selectDatabase...");
    }


    public void foo() {
        System.out.println("noLog...");
    }
}

\quad接下来使用动态字节码增强技术实现功能,这一步可选的方法有很多:
在这里,使用byte-buddy实现,当然也有中文版的:Byte Buddy 教程,先引入ByteBuddy maven仓库。接着根据教程中的代码,修改成符合自己需求的代码:

/**
     * 实现动态增强字节码
     * @return 返回新的AnnotationPractise实例
     * @throws NoSuchMethodException 方法没有找到
     * @throws IllegalAccessException 访问了private方法
     * @throws InvocationTargetException 被访问的方法丢出了异常,但是没有被接收
     * @throws InstantiationException 不能创建这样一个实例,比如说是抽象类和接口
     */
    private static AnnotationPractise enhanceAnnotation() throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        return new ByteBuddy()
                //动态的生成AnnotationPractise的子类
                .subclass(AnnotationPractise.class)
                //匹配带Log注解的方法
                .method(ElementMatchers.isAnnotatedWith(Log.class))
                //将方法拦截并委托给LoggerInterceptor
                .intercept(MethodDelegation.to(LoggerInterceptor.class))
                //创建一个新的AnnotationPractise实例
                .make()
                .load(Main.class.getClassLoader())
                .getLoaded()
                .getConstructor()
                .newInstance();
    }
    
 /**
     * 实现截取父类中带Log注解的方法,然后添加输出语句
     */
    public static class LoggerInterceptor {
        public static void log(@SuperCall Callable<Void> zuper)
                throws Exception {
            System.out.println(LocalTime.now());
            try {
                zuper.call();
            } finally {
                System.out.println(LocalTime.now());
            }
        }
    }
}

效果如下:

08:51:56.916
selectDatabase...
08:51:56.917
08:51:56.917
queryDatabase...
08:51:56.917

Process finished with exit code 0

\quad可以看到,我没有对源代码做任何修改,就实现了对源代码功能的增强。从这里可以看出,注解就是为我们自己的业务逻辑所服务的,再次印证了那句话“如果对注解不做处理,那么注解也不会比注释有用”。

  • 2.实现一个基于@Cache注解的装饰器,能够将传入的服务类的Class进行装饰,使之具有缓存功能

\quad 意思就是模拟数据库的查找操作,第一次查找的时候将获取的数值加入到缓存中,如果第二次查找的还是这个数则直接返回这个数。这样就省去了重复查找同一个数的时间。
实现思路:
\quad 前部分与上例中类似,显示截取类中带有Cache注解的方法,对其实现动态字节码增强。
新建注解:

@Retention(RetentionPolicy.RUNTIME)
public @interface Cache {
    // 标记缓存的时长(秒),默认60s
    int cacheSeconds() default 60;
}

新建两个测试方法,一个带注解一个不带

public class DataService {
    /**
     * 根据数据ID查询一列数据,有缓存。
     *
     * @param id 数据ID
     * @return 查询到的数据列表
     */
    @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());
    }

    /**
     * 根据数据ID查询一列数据,无缓存。
     *
     * @param id 数据ID
     * @return 查询到的数据列表
     */
    public List<Object> queryDataWithoutCache(int id) {
        // 模拟一个查询操作
        Random random = new Random();
        int size = random.nextInt(10) + 1;
        return IntStream.range(0, size)
                .mapToObj(i -> random.nextBoolean())
                .collect(Collectors.toList());
    }
}

使用ByteBuddy增强原方法:

@SuppressWarnings("unchecked")
    public static <T> Class<T> decorate(Class<T> klass) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        return (Class<T>) new ByteBuddy()
                //子类化,为原先的类添加新的功能
                .subclass(klass)
                .method(ElementMatchers.isAnnotatedWith(Cache.class))
                .intercept(MethodDelegation.to(CacheInterceptor.class))
                .make()
                .load(klass.getClassLoader())
                .getLoaded();
    }

拦截器中的具体内容:

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

    @RuntimeType //这个注解的作用在于当拦截方法发生时,ByteBuddy能够找到对应的方法去调用它
    public static Object cache(
            //声明调用父类的方法
            @SuperCall Callable<Object> superCall,
            //当前正在被调用的方法
            @Origin Method method,
            //实体方法的对象
            @This Object object,
            //获得所有的参数
            @AllArguments Object[] arguments) throws Exception {
        CacheKey cacheKey = new CacheKey(method.getName(), object, arguments);
        CacheValue value = hashMap.get(cacheKey);
        if (value != null) {
            //缓存中存在对应的值直接返回
            if (isNotOvertime(method, value)) {
                return value.result;
            }
            return getObject(superCall,cacheKey);
        } else {
            //否则调用父类中的方法,拿到值
            //方法一:使用method.invoke(object,arguments)调用方法,这样很慢
            //方法二:使用ByteBuddy提供的@SuperCall调用父类方法
            return getObject(superCall, cacheKey);
        }
    }

    private static boolean isNotOvertime(@Origin Method method, CacheValue value) {
        return (System.currentTimeMillis()-value.time)<method.getAnnotation(Cache.class).cacheSeconds()*1000;
    }

    private static Object getObject(@SuperCall Callable<Object> superCall, CacheKey cacheKey) throws Exception {
        Object realResult = superCall.call();
        long time = System.currentTimeMillis();
        CacheValue cacheValue = new CacheValue(realResult,time);
        hashMap.put(cacheKey, cacheValue);
        return realResult;
    }

    static class CacheValue{
        private Object result;
        private long time;

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

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

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


        //Map的key遵循的是equals和hashcode约定,CacheKey必须重写equals和hashCode方法
        @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(methodName, cacheKey.methodName) &&
                    Objects.equals(thisObject, cacheKey.thisObject) &&
                    Arrays.equals(arguments, cacheKey.arguments);
        }

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

5.小结

\quad 其实照这样看来,注解本身起不了多大的作用,关键是看程序员或者框架本身对注解的理解与使用。所以,一方面要熟记基本语法,另一方面想深究的话可以去看下框架对注解具体是怎么处理的。

6.参考资料

1.《Java编程思想》[第四版] (美)Bruce Eckel 著;陈浩鹏译.——北京:机械工业出版社.2007.6(2018.9重印)
2.掘金.《Java注解详解》点击此处查看源文章
3.博客园.《@SuppressWarning 抑制警告注解》点击此处查看源文章