最近在好好整理自己学过的一些知识,打算把他们都好好地记录下来,完善个人的知识体系,坚持!冲冲冲
Spring、Mybatis、lombok等等等等,我们开发程序时接触了很多框架,他们大多都提供了自己实现的注解,熟练的使用可以使其帮助我完成我的程序,但是他是如何做到的,一个小小的注解是如何实现如此强大功能的,由这个问题的思考我决定仔细研究一下Java的注解机制
注解由JDK5引入,是Java对于元数据的支持。用人话说就是一种在代码里面的特殊标记,这些标记可以在一些时期被读取并且执行,因此开发人员就可以在不改变原有业务代码逻辑的基础上,增强我们的程序。
以上这几个注解经常出现则各种源码中,说实话,在系统的整理一边注解知识前,对于注解我总是一脸懵逼的,写的啥啊,完全不懂啊????
看过一些书籍整理了一下后豁然开朗
下面就且听我慢慢道来
:warning:注意:这个注解只能被用在方法上(为什么下面会说)
这个注解非常的常见,每次重写父类方法或者实现接口方法时,智能的开发工具都会贴心的自动帮我加上这么一个注解,开始我是拒绝的,后来才知道这个加上这个注解是为了帮助开发者检查对于父类方法的实现是否正确。
举个栗子,如果父类里面有个叫做hello的方法,那么继承它的子类可以对他重写,万一失手错写成了helo,虽然没有正确的重写,但是也不算错,因为它可以被看为子类的一个新的方法,此时程序可以正确执行。但是如果在helo的方法上加上@Override注解,此时编译器就知道这个方法是子类要对于父类方法进行重写,就会自动的检查对于父类方法是否重写正确(很明显父类没有helo方法,我们需要重写的是hello(),那此时就会编译出错)
扯了一堆,一句话就是@Override就是帮助开发者核查重写方法是否正确的(方法名参数返回值等的核查)
:warning:注意:这个注解可以被用在类、方法上
这个注解的意思非常的简单,就是用来标注出过时的代码的
public class TestAnnotation { public static void main(String[] args) { new DeprecatedClass().info(); } } // 下面定义了一个类,加了@Deprecated表示他是过时的 // 类中有一个方法,也是过时的 23333 @Deprecated class DeprecatedClass { @Deprecated public void info() { System.out.println("giao"); } } 复制代码
如果编译此程序,会给出警告,这个类和这个方法都过时了,但是不影响程序执行
这个注解可以帮助消除编译时的警告提示
举个栗子,我的程序里面有一个方法sayHello(),此方法返回一个String,我在调用此方法时,只是调用但是忽略使用了这个方法的返回值,那么在我编译这个程序的时候,就会有警告我忽略的这个返回值,此时我在可以在方法上加上@SuppressWarnings("unused"),unused就代表未使用警告,此使用方式就会告诉编译器忽略这个警告,编译时就不会发出这个警告了,眼不见为净
public class TestAnnotation { public static void main(String[] args) { // 如果我不加这个注解就会有警告,but你可以不理他,警告并不会对程序运行有影响 @SuppressWarnings("unused") String string = new SuppressWarningClass().sayHello(); } } class SuppressWarningClass { public String sayHello() { return "hello"; } } 复制代码
以上三个注解就是Java提供给我们的几个基础的注解(当然不止这些),举例说明只是为了帮助我们快速了解注解,快速的试试看注解咋玩,下面我们深入的对其进行解读,了解其背后的秘密
一个注解的定义一定少不了的就是元注解,Java自带了几个元注解帮助修饰其他的注解的定义,下面一一介绍
这个注解只能被修饰于其他的注解,它用来指定被修饰的注解的保存时间,@Retention注解包含一个RetentionPolicy类型的成员变量value,我们通过指定value的值来设置保存时间(指定的方法其实就是@Retention(value=RetentionPolicy.RUNTIME)这样,通过在括号中传入对应成员变量name=value的方式指定)
对于注解的保存时间,@Retention的value只能由三种
@Retention(RUNTIME) // 设置其可存活的时间 public @interface MyAnnotation { } 复制代码
by the way, 如果注解里面只需要为一个名为value的成员变量赋值,可以不需要name=value的方式去指定,直接写就行。上面的栗子中,@Retention(RUNTIME) 就是利用了此中方式,直接为value进行赋值
这个注解也是用于修饰注解的注解,它用来指定被修饰的注解只能用于程序单元,比如上面介绍的的@Override注解,它就是因为@Target的指定只能被修饰于方法之上。
@Target的value有很多,它有一个ElementType的枚举:
@Target的源码是这样
@Documented @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.ANNOTATION_TYPE) public @interface Target { /** * Returns an array of the kinds of elements an annotation type * can be applied to. * @return an array of the kinds of elements an annotation type * can be applied to */ ElementType[] value(); } 复制代码
它就被定义为只能修饰注解,它有一个成员变量value,所以我们可以直接在括号里面定义的值,就像我上面说的,不需要加value=,可以注意到此处value的类型是ElementType[],这是一个数组,那么意味着,如果我们需要定义多个,直接以数组形式进行指定就可以,像下面这样
@Target({CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, PARAMETER, TYPE}) 复制代码
这个注解相较于上面两个的作用较小,通过它的名字就可以看出是和文档相关
被这个注解修饰的注解,在javadoc生成的文档中可以被查看到,仅此而已...
被此注解修饰的注解,将具有继承性,举个例子,有一个注解X被@Inherited修饰,如果A类加上了X注解,那么它的子类都会自动的加上X注解(悄悄地加上,通过查看编译后的代码可以看到哦~)
上面铺垫了许多,下面我们来尝试自己定义注解
定义一个注解和定义一个接口一样相似,接口的关键字是interfac,而注解的关键字是@interface(就是在接口的基础上加一个@)
public @interface MyAnnotation { } 复制代码
使用这个注解也十分的简单,直接在你想要使用的头部加上即可
@MyAnnotation class TestMyAnnotation { @MyAnnotation public void testMethod() { } } 复制代码
注解可以有自己的成员变量
public @interface MyAnnotation { String value(); int age(); } 复制代码
在定义了成员变量后,使用时必须要为他们赋值
@MyAnnotation(value = "test", age = 123) class TestMyAnnotation { @MyAnnotation(value = "abc", age = 234) public void testMethod() { } } 复制代码
当然也可以给他们设置一个默认值,这样使用时就不一定需要赋值,不赋值时就直接使用的是默认值
使用defalut关键字即可为其实现设置默认值
public @interface MyAnnotation { String value() default "abc"; int age() default 123; } 复制代码
根据是否拥有成员变量,Annotation可以被分为如下两种:
说了那么多的Annotation,也介绍了如何自定义一个Annotation,问题来了,这些Annotation到底是如何发挥作用的呢?我们继续探索
一个Annotation只凭借它本身是没有任何作用的,必须要开发者提供相应的工具提取其信息才可以使其发挥作用。
我们定义一个Annotation,其实本质上是实现了Java提供的Annotation接口,Annotation接口是所有注解的父接口。同时,Java5引入了AnnotatedElement接口,这个接口是所有程序元素(比如class,interface,method等)的父接口,所以通过反射,并利用AnnotatedElement提供给我们的方法就可以取得注解中的信息,废话少说,直接上代码
AnnotatedElement提供了多个非常有用的方法帮助我们获取注解的信息
public void getMyAnnotation() throws NoSuchMethodException, SecurityException, ClassNotFoundException { Annotation[] annotations = Class.forName("com.learn.javase.TestMyAnnotation").getMethod("testMethod").getAnnotations(); for (Annotation annotation : annotations) { System.out.println(annotation); } } 复制代码
上述代码就是最简单的获取方法上的Annotation的方法,这里我们就是使用了AnnotatedElement提供的getAnnotations方法,获取到了此程序元素上的所有的注解,并循环打印了出来
进一步的,如果想要获取注解中的成员变量值也是很简单的
public void getMyAnnotation2() throws NoSuchMethodException, SecurityException, ClassNotFoundException { Method[] methods = Class.forName("com.learn.javase.TestMyAnnotation").getMethods(); for (Method method : methods) { MyAnnotation myAnnotation = method.getAnnotation(MyAnnotation.class); // 此处需要做一个判空 if (myAnnotation != null) { System.out.println("value="+myAnnotation.value()+",age="+myAnnotation.age()); } } } 复制代码
Java8前其实也存在重复注解,只不过通过一种曲线的形式实现的。首先Java是不允许一个程序元素上出现两个一样的注解的,比如下面这种,程序编译会直接报错
@MyAnnotation(value = "test", age = 123) @MyAnnotation(value = "test", age = 234) class TestMyAnnotation3 { } 复制代码
但是可以通过一种特殊的方式实现这种操作,就是定义一个注解容器,如:
@Retention(RUNTIME) // 设置其可存活的时间 public @interface MyAnnotationColloection { MyAnnotation[] value(); } 复制代码
通过这样的定义,就可以以一种奇怪的姿势实现重复注解,像下面这样:
@MyAnnotationColloection({@MyAnnotation(value = "test", age = 123),@MyAnnotation(value = "test", age = 123)}) class TestMyAnnotation3 { } 复制代码
这个地方要注意一下,容器的保存时间一定要大于或者等于具体的注解
但是,这样实在是不太美观,所以Java8引入了@Repeatable注解,我们在定义@MyAnnotation的时候可以加上@Repeatable注解进行修饰,value传入容器的class,像下面这样:
@Repeatable(MyAnnotationColloection.class) @Retention(RUNTIME) // 设置其可存活的时间 public @interface MyAnnotation { String value() default "abc"; int age() default 123; } 复制代码
那么现在我们就可以更加美观的使用重复注解了
@MyAnnotation(value = "test", age = 123) @MyAnnotation(value = "test", age = 123) //@MyAnnotationColloection({@MyAnnotation(value = "test", age = 123),@MyAnnotation(value = "test", age = 123)}) class TestMyAnnotation3 { } 复制代码
但是这种方式说白了,其本质还是和最开始介绍的一样。新的写法只是老方法的一种简化,因为它依旧是以容器的形式进行重复定义
在Java8中 ElementType 新增两个枚举成员,TYPE_PARAMETER 和 TYPE_USE ,在Java8前注解只能标注在一个声明(如字段、类、方法)上,Java8后,新增的TYPE_PARAMETER可以用于标注类型参数,而TYPE_USE则可以用于标注任意类型(不包括class)。
如果定义为此范围,那么注解可以被修饰在如下几个位置:
class D<@Parameter T> { }class Image implements @Rectangular Shape { }new @Path String("/usr/bin") String path=(@Path String)input;if(input instanceof @Path String)public Person read() throws @Localized IOException.List<@ReadOnly ? extends Person>List<? extends @ReadOnly Person> @NotNull String.class import java.lang.@NotNull String 复制代码
关于此概念此处我只做一下简单的介绍,因为个人接触的十分有限。
APT(Annotation Processing Tool 注解 处理工具)是处理代码中的注解, 用来生成代码, 换句话说, 这是用代码生成代码的工具。使用它的目的是简化开发者的工作量。Annotation处理器的定义需要继承一个AbstractProcessor父类,通过它提供的方法我们可以用反射以外的方式去获取这个Annotation的信息并进行你想要的操作。
在实现了具体的APT后,它的运行需要依赖特殊的javac命令
javac -processor APT的类名 具体的java文件.java 复制代码