美文网首页
Java 注解的使用

Java 注解的使用

作者: AlienPaul | 来源:发表于2018-07-21 15:26 被阅读0次

    Annotation的分类

    注解为JDK1.5引入的新内容,调用形式为@Annotation
    注解的本质是接口。
    注解不影响Java代码的执行。但在运行时,可以通过一些手段例如反射,获取注解的信息,并对其进行处理。

    Annotation分为如下3类:

    1. JDK系统注解
    2. 元注解
    3. 自定义注解

    JDK系统注解

    @Override

    @Override注解只能使用在方法上。它用来标识出该方法是用来重写或实现父类或者接口方法的。例如:

    class A {
        public void fun1() {}
    }
    
    class B extends A {
        @Override // 重写A的fun1方法
        public void fun1() {}
    }
    
    interface I {
        void fun2();
    }
    
    class C implements I {
        @Override // 实现接口I的fun2方法
        public void fun2() {}
    }
    

    下面问题来了,大家会发现,如果删除上面例子中的@Override注解,代码并不会有任何错误。那么要这个注解有什么用呢?
    有这么一个例子,我们写了一个Student类,需要重写它的toString方法:

    class Student {
        private String name;
        private int age;
        // 省略setter和getter
    
        public String tostring() { //这里大家发现问题了吗?
            return "name: " + this.name + " age: " + this.age;
        }
    }
    

    很不幸的是这里程序员粗心的把toString写成了tostring。但是IDE不会有任何错误提示。IDE认为我们的意图是定义一个新方法tostring,而不是去重写方法toString。
    我们尝试在错误的tostring方法上加入override注解:

    @Override
    public String tostring() {
        return "name: " + this.name + " age: " + this.age;
    }
    

    编辑器会有如下错误提示:


    Screen Shot 2018-07-21 at 10.27.33 am.png

    到这里大家一定都意识到@Override注解的重要性了吧。
    这里总结一下,如果定义方法的目的就是为了实现或重写其他方法,务必要加上@Override注解。

    @Deprecated

    如果一个方法有了更好的替代品,为了兼容性暂时保留但是不建议其他人继续使用需要怎么办?这时候@Deprecated注解派上用场了。调用被@Deprecated修饰的方法会得到编辑器的警告,同时会被标记为删除线:


    Screen Shot 2018-07-21 at 10.42.10 am.png

    @SuppressWarnnings

    编译器很聪明会自动检查代码中的问题给予我们警告。接着上面的例子,如果我们确实需要调用一个被标记为deprecated的方法,又不想忍受编辑器的警告,难道就没有办法了吗?@SupressWarnings可以帮我们这个忙。


    Screen Shot 2018-07-21 at 10.48.39 am.png

    我们发现加入了@SuppressWarnings注解后,编译器的告警消失,并且对deprecated方法的调用也不会被标记上删除线。

    @FunctionalInterface

    该注解为Java 8 之后新增加的注解,目的是为了配合新增加的lambda表达式使用。具体Lambda表达式如何使用在这里暂不介绍,请关注本人其他的博客。

    Java 8 新增加的Lambda表达式体现了函数式编程的思想,本质上仍然是一个匿名内部类,但是该匿名内部类中只能有一个未实现的方法。
    为了约束接口中的抽象方法数量,引入了@FunctionalInterface接口
    其作用为被该注解修饰的接口,里面的抽象方法有且只能有一个。如果不符合条件会给出警告。

    public class Demo {
        public static void main(String[] args) {
            MyList<String> myList = new MyList<>();
    
            myList.addItems(Arrays.asList("abc", "def", "ghi"));
    
            // 使用Lambda表达式
            myList.myForEach(s -> {
                String uppercaseString = s.toUpperCase();
                System.out.println(uppercaseString);
            });
        }
    }
    
    class MyList<T> {
        private List<T> list = new ArrayList<>();
    
        public void addItems(List<T> itemList) {
            list.addAll(itemList);
        }
    
        // 传入实现了MyForEachFunction接口的对象。使用该方法可以传入lambda表达式
        public void myForEach(MyForEachFunction<T> fun) { 
            for (T t : list) {
                fun.doForEach(t);
            }
        }
    }
    
    @FunctionalInterface
    interface MyForEachFunction<T> {
        void doForEach(T t); // 只能有一个抽象方法
    }
    

    元注解

    元注解为修饰其他注解的注解,在创建自定义注解的时候及其有用。下面我们介绍下JDK中的元注解。

    @Retention

    @Retention指明注解是如何被存储的。其参数有3个选项:

    • RetentionPolicy.SOURCE 仅在源代码中出现,注解会被编译器忽略。
    • RetentionPolicy.CLASS 该注解在编译时会被编译器读取。但会被JVM忽略。
    • RetentionPolicy.RUNTIME 注解在运行时会被JVM获取到。能够使用Java的反射API读取。这个选项使用的最为广泛。

    @Target

    @Target是用来限制注解使用的范围。它的参数是ElementType。下面贴出ElementType的代码:

    public enum ElementType {
        /** Class, interface (including annotation type), or enum declaration */
        // 用于修饰class, interface,@interface和enum类型声明
        TYPE,
    
        /** Field declaration (includes enum constants) */
        // 修饰成员变量,包括enum中的常量
        FIELD,
    
        /** Method declaration */
        // 方法声明
        METHOD,
    
        /** Formal parameter declaration */
        // 参数声明,比如Spring MVC中的@RequestParam
        PARAMETER,
    
        /** Constructor declaration */
        // 构造函数
        CONSTRUCTOR,
    
        /** Local variable declaration */
        // 局部变量
        LOCAL_VARIABLE,
    
        /** Annotation type declaration */
        // 注解类型声明
        ANNOTATION_TYPE,
    
        /** Package declaration */
        // 包声明
        PACKAGE,
    
        /**
         * Type parameter declaration
         *
         * @since 1.8
         */
        // 用于泛型类型,例如class A<@Annotation T> {}
        TYPE_PARAMETER,
    
        /**
         * Use of a type
         *
         * @since 1.8
         */
        // 经试验,除了package和返回值为void的方法,其他位置都可以使用
        TYPE_USE
    }
    

    @Documented

    @Documented注解表明使用Java Doc工具的时候,被该注解修饰的注解会出现在生成的Javadoc中。

    @Inherited

    标记为@Inherited的注解,修饰的class被其他类继承之时,该注解能够一并继承过去。下面以一段代码为例:
    定义一个annotation

    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.TYPE)
    @Inherited //启用了注解继承
    public @interface MyAnnotation {
    }
    
    public class Test {
        public static void main(String[] args) {
            System.out.println(B.class.isAnnotationPresent(MyAnnotation.class)); //@MyAnnotation修饰的是class A,class B继承自class A,因此class B是被MyAnnotation修饰的
            if (B.class.isAnnotationPresent(MyAnnotation.class)) {
                MyAnnotation annotation1 = B.class.getDeclaredAnnotation(MyAnnotation.class);
                System.out.println(annotation1); //返回null。B没有直接被MyAnnotation修饰
                MyAnnotation annotation2 = B.class.getAnnotation(MyAnnotation.class);
                System.out.println(annotation2); //返回@com.paultech.MyAnnotation()。
            }
        }
    }
    
    @MyAnnotation
    class A {
    }
    
    class B extends A {
    }
    

    @Repeatable

    @Repeatable表示该注解可以修饰同一元素多次。即能够像如下这种方式使用:

    @Descripor("Hello")
    @Descripor("World")
    class SomeClass {}
    

    下面介绍下如何定义自己的repeatable注解。

    @Retention(RetentionPolicy.RUNTIME)
    @Repeatable(DoSomethingList.class) //需要指定一个容器注解
    @interface DoSomething {
    }
    
    @Retention(RetentionPolicy.RUNTIME)
    @interface DoSomethingList {
        DoSomething[] value(); // 这里必须为value
    }
    

    通过反射获取注解:

    @DoSomething
    @DoSomething //这里使用两个DoSomething
    public class RepeatableTest {
        public static void main(String[] args) {
            DoSomething[] declaredAnnotationsByType = RepeatableTest.class.getDeclaredAnnotationsByType(DoSomething.class);
            System.out.println(declaredAnnotationsByType.length); // 输出为2
        }
    }
    

    自定义注解

    Annotation的定义

    注解使用@interface 定义,语法如下:

    public @interface MyAnnotation {
        // ...属性定义
    }
    

    属性定义的语法为:

    类型 字段名() [default] [defaultValue]
    

    例如:

    public @interface MyAnnotation {
        String value();
    }
    

    使用value作为属性名比较特殊,调用时可以显式指定属性名,也可以不指定:

    @MyAnnotation(value = "Hello") // 显式指定value
    class SomeClass {}
    
    @MyAnnotation("world") // 不指定,默认为value属性
    class AnotherClass {}
    

    有一点需要格外注意的是,属性定义的字段类型必须为Java基本数据类型,再加上String和注解类型本身,或者他们的数组。

    @interface Something {
        String[] strArr(); // String数组,合法
        int age(); // int类型,合法
        Integer someInt(); // 其他引用类型,不合法
        Another another(); // 注解类型,合法
    }
    
    @interface Another {}
    

    如果定义了数组类型的属性,使用该注解时可以通过如下语法传入值:

    @MyAnnotation(key = {"Hello", "World"})
    class SomeClass {}
    
    @MyAnnotation(key = "Hi Paul") //尽管key是String[]类型,如果只想传入一个值,仍然可以通过这种方式
    class Another {}
    

    注解相关的反射API

    上文提到过,注解被指定为RUNTIME的时候,可以通过Java反射,在运行时获取到该注解。
    以一段代码为例:

    @SendSomething("Wahaha")
    public class GetAnnotationDemo {
    
        public static void main(String[] args) {
        // 获取修饰GetAnnotationDemo的SendSomething类型注解
            SendSomething declaredAnnotation = GetAnnotationDemo.class.getDeclaredAnnotation(SendSomething.class);
    
            System.out.println(declaredAnnotation.value()); // 返回Wahaha
        }
    }
    
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.TYPE)
    @interface SendSomething {
        String value() default "defaultValue";
    }
    

    以上是注解最简单的使用。与注解有关的其他反射方法总结如下:

    方法名 描述
    isAnnotationPresent(A.class) 是否被A类型注解修饰
    getDeclaredAnnotation(A.class) 获取类型为A的直接修饰的注解实例
    getDeclaredAnnotationsByType(A.class) 获取类型为A的直接修饰的注解实例, 返回数组
    getDeclaredAnnotations() 获取所有直接修饰的注解实例,返回数组
    getAnnotation(A.class) 获取类型为A的注解实例,返回数组
    getAnnotationsByType(A.class) 获取所有类型为A的注解实例,返回数组
    getAnnotations() 获取所有注解实例,返回数组

    注解的使用场景

    在这个例子中我们要实现读取properties文件的内容并自动注入到class当中。
    conf.properties文件:

    username=paul
    password=123456
    
    @PropertySource("/path/to/conf.properties")
    class UserConf {
        // 自动读取出username和password
        String username;
        String password;
    }
    

    下面代码给出了实现该功能的主要逻辑。
    定义注解:

    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.TYPE)
    public @interface PropertySource {
        String value();
    }
    

    编写主要逻辑:
    PropertyResolver.java

    public class PropertyResolver {
        public <T> T getProperty(Class<T> propertySourceClass) throws Exception {
            T propertySourceBean = propertySourceClass.newInstance();
    
            // 如果propertySourceClass被PropertySource注解修饰
            if (propertySourceClass.isAnnotationPresent(PropertySource.class)) {
                
                PropertySource propertySource = propertySourceClass.getDeclaredAnnotation(PropertySource.class);
    
                File propertyFile = new File(propertySource.value());
                // 读取properties文件内容到properties
                FileReader fileReader = new FileReader(propertyFile);
                Properties properties = new Properties();
                properties.load(fileReader);
                
                // 装配属性
                // 获取propertySourceClass所有的成员变量
                Field[] declaredFields = propertySourceClass.getDeclaredFields();
                // 获取属性文件中所有的key
                Set<String> propertyNames = properties.stringPropertyNames();
    
                for (Field declaredField : declaredFields) {
                    String fieldName = declaredField.getName();
                    for (propertyName: propertyNames) {
                        if (fieldName.equals(propertyName)) {
                            // 如果成员变量不可访问,设置为能够访问
                            if (!declaredField.isAccessible()) {
                                declaredField.setAccessible(true);
                            }
                            // 设置属性值
                            declaredField.set(propertySourceBean, properties.getProperty(propertyName));
                            break;
                        }
                    }
                }
            } 
            return propertySourceBean;
        }
    }
    

    使用自己编写的工具

    public static void main(String[] args) {
        UserConf userConf = new PropertyResolver().getProperty(UserConf.class);
        userConf.username; // "paul"
        userConf.password; // "123456"
    }
    

    完整代码实现请点击链接: https://github.com/paul8263/PropertyResolver

    本博客为作者原创,欢迎大家参与讨论和批评指正。如需转载请注明出处。

    参考资料

    Oracle Predefined annotation types. https://docs.oracle.com/javase/tutorial/java/annotations/predefined.html

    相关文章

      网友评论

          本文标题:Java 注解的使用

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