Java的注解

Java的注解(annotation)

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

@Override
    public int hashCode() {
        return Objects.hash(XXX);
    }
复制代码

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

1.注解的基本语法

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

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

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

语法以下:

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.注解元素

在注解中一样能够声明成员变量。可是对成员变量的类型有要求:bash

  • 全部的基本类型
  • String类型
  • Class
  • enum
  • Annotation

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

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

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

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 抑制警告注解》点击此处查看源文章

相关文章
相关标签/搜索