Java注解一篇吃透
什么是注解?
Java 注解又称 Java 标注,是 JDK5 中出现的新特性,注解是放在Java源码的类、方法、字段、参数前的一种特殊“注释”,注释会被编译器直接忽略,注解则可以被编译器打包进入class文件,进行执行(一些注解不会被编译器编译进入 .class 文件,它们在编译后就被编译后扔掉),更深入的理解,注解就是代码的元数据(metadata),在 JDK5 的官方文档中就把注解描述为元数据
元数据就是描述数据的数据,类似于 Windows 系统下的文件属性
为什么会出现注解?
Many APIs require a fair amount of boilerplate code. For example, in order to write a JAX-RPC web service, you must provide a paired interface and implementation. This boilerplate could be generated automatically by a tool if the program were “decorated” with annotations indicating which methods were remotely accessible.
Other APIs require “side files” to be maintained in parallel with programs. For example JavaBeans requires a
BeanInfoclass to be maintained in parallel with a bean, and Enterprise JavaBeans (EJB) requires a deployment descriptor. It would be more convenient and less error-prone if the information in these side files were maintained as annotations in the program itself.出自 JDK5注解官方文档
翻译:
许多 API 需要大量的样板代码。例如,为了编写 JAX-RPC Web 服务,您必须提供成对的接口和实现。如果程序被“装饰”了说明哪些方法可以远程访问的注释,则该样板可以由工具自动生成。
其他 API 需要与程序并行维护“辅助文件”。例如,JavaBeans 需要 BeanInfo 类与 bean 并行维护,而 Enterprise JavaBeans (EJB) 需要部署描述符。如果这些辅助文件中的信息在程序本身中作为注释进行维护,将会更加方便且不易出错。
注解的分类
我们先来了解元注解再来学习其他注解,可以帮助我们能更好理解注解以及前面所留下的问题,为什么一些注解不会进入编译后的 .class 文件中
元注解
元注解就是标注注解的注解,元注解都可以在 java.lang.annotation 包中找到,JDK8官方文档
我们先来了解 JDK5 出现的注解
@Documented
@Documented 注解表明这个注解应该被 javadoc工具记录. 默认情况下,javadoc是不包括注解的. 但如果声明注解时指定了 @Documented,则它会被 javadoc 之类的工具处理, 所以注解类型信息也会被包括在生成的文档中,是一个标记注解,没有成员。
Javadoc 是自动生成java文档的工具,在 idea 的 tools 中可以使用
(注意高版本的 idea 在使用低版本的 JDK 会出现 javadoc: 错误 - 无效的标记: --source-path错误,请自行提升项目的 JDK 版本)
案例:
先定义一个自定义注解(后面会将)
package com.hfly.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Target;
@Documented//此时加上了 @Documented 注解
@Target({ ElementType.TYPE, ElementType.METHOD })
public @interface MyDocumented {
String value() default "这是@Documented注解";
}
在方法上标注该注解
package com.hfly;
import com.hfly.annotation.MyDocumented;
public class Annotation {
@MyDocumented
public static void main(String[] args) {
}
}
查看生成的文档
可以看的 main 方法上出现了注解 @MyDocumented
现在我们删除 MyDocumented 注解上的 @Documented 元注解,重新生成文档,查看 main 方法
可以看到 main 方法上的,@MyDocumented 注解消失
@Inherited
@Inherited 的作用在于类的继承,如果一个注解使用了 @Inherited 元注解,并且该注解被标记在一个父类 A 上,则 A 类的子类会自动继承该注解
注意:被 @Inherited 标注的注解,只有作用于类上自动继承效果才会生效,总用于字段和方法上无效
案例:
创建一个自定义注解 MyInherited
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface MyInherited {
String value();
}
创建一个父类 A
@MyInherited("使用 @MyInherited ,作用于类上")
public class A {
}
创建一个子类 B
public class B extends A{
}
使用反射进行测试(后面文章会详细介绍,此时无需担心)
public static void main(String[] args) throws NoSuchFieldException {
Class<A> a = A.class;
System.out.println("A类:"+a.getAnnotation(MyInherited.class).value());
Class<B> b = B.class;
System.out.println("B类:"+b.getAnnotation(MyInherited.class).value());
}
此时,删除 MyInherited 注解上的 @Inherited 元注解,观察结果
@Retention
@Retention 元注解来标注一个注解的保留时间,如果一个注解未标注 @Retention 注解,则默认的保留策略为:RetentionPolicy.CLASS
此前我们留下的伏笔,一些注解不会被编译进入 .class 文件,就是因为这个注解在从中作祟
我们来观察 RetentionPolicy 的源码
public enum RetentionPolicy {
/**
* Annotations are to be discarded by the compiler.
* 注解将会被编译器丢弃
*/
SOURCE,
/**
* Annotations are to be recorded in the class file by the compiler
* but need not be retained by the VM at run time. This is the default
* behavior.
* 注解会被编译器加载进Class文件,但不需要在 VM 运行时保留,这是默认的方案
*/
CLASS,
/**
* Annotations are to be recorded in the class file by the compiler and
* retained by the VM at run time, so they may be read reflectively.
*
* @see java.lang.reflect.AnnotatedElement
* 注解会被编译器加载进class文件,并且在 vm 运行时保留,所以我们可以通过反射的方式读取他们
*/
RUNTIME
}
下面我就通过三个案例来演示三种不同的保留策略
首先我们先创建一个注解
@Retention(RetentionPolicy.SOURCE)//此时的保留策略为 SOURCE
public @interface MyRetention {
}
在 main方法上标注注解
public class Annotation {
@MyRetention
public static void main(String[] args) throws NoSuchFieldException, NoSuchMethodException {
Class<Annotation> annotation = Annotation.class;
System.out.println(annotation.getMethod("main", String[].class).getAnnotation(MyRetention.class));
}
}
观察编译后的.class文件
修改保留等级为 CLASS,观察编译后的文件
当执行结果为 null
继续修改保留等级为 RUNTIME,观察编译后的文件,编译后的文件和上图一致,当执行结果从 null 变为了 @xxxxxx.xxxx.MyRetention()
@Target
@Target 用于标记一个注解的使用范围,不标记 @Target 元注解的注解可以作用在任何地方,如果要标记的使用范围必须使用 java.lang.annotation.ElementType 枚举类型的一个或多个值,java.lang.annotation.ElementType 包含的作用范围有
public enum ElementType {
/** Class, interface (including annotation type), or enum declaration */
//类,接口(包括注解类型),枚举
TYPE,
/** Field declaration (includes enum constants) */
//字段(包括枚举类型里的枚举常量)
FIELD,
/** Method declaration */
//方法
METHOD,
/** Formal parameter declaration */
//方法参数
PARAMETER,
/** Constructor declaration */
//构造器
CONSTRUCTOR,
/** Local variable declaration */
//局部变量
LOCAL_VARIABLE,
/** Annotation type declaration */
//注解类型
ANNOTATION_TYPE,
/** Package declaration */
//包
PACKAGE,
/**
* Type parameter declaration
* 泛型,出自JDK8
* @since 1.8
*/
TYPE_PARAMETER,
/**
* Use of a type
* 只要是型态名称,都可以进行标注,出自JDK8
* @since 1.8
*/
TYPE_USE
}
具体实例参考:StackOVerflow注解文章
上文的文章中没有 TYPE_PARAMETER 和 TYPE_USE
@Target(ElementType.TYPE_PARAMETER)
public @interface Email {}
public class MailBox<@Email T> {
...
}
@Target(ElementType.TYPE_USE)
public @interface Test {}
List<@Test Comparable> list1 = new ArrayList<>();
List<? extends Comparable> list2 = new ArrayList<@Test Comparable>();
@Test String text;
text = (@Test String) new Object();
java.util. @Test Scanner console;
console = new java. util. @Test11 Scanner(System.in);
以上的代码均来源于 【JDK8】Annotation 功能增强
我们再来了解一下 JDK8 新增的注解
@Native
使用 @Native 注解修饰成员变量,则表示这个变量可以被本地代码引用,常常被代码生成工具使用。编译后被编译器抛弃,使用不多
自我认为 @Native 并不属于元注解,但是多数人都称为元注解,我们来看一下 @Native 的源码:
@Documented
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.SOURCE)
public @interface Native {
}
可以看到 @Native 的作用范围不是注解而是字段,但是我还是将他放入元注解类中,分类只要大家认可就行,哈哈
@Repeatable
@Repeatable 注解的作用是允许对相同的字段重使用一个注解,当一个注解没有标记 @Repeatable 的时候,只能对一个字段使用一次,可以使用注解容器对一个字段进行同注解的多次标注
public @interface MyRepeatable {
}
//注解容器
public @interface MyRepeatables {
MyRepeatable[] value();
}
public class Annotation {
@MyRepeatables( value = {@MyRepeatable,@MyRepeatable})
private String a;
@MyRetention
public static void main(String[] args) throws NoSuchFieldException, NoSuchMethodException {
}
}
使用 @Repeatable 后
@Repeatable(MyRepeatables.class)
public @interface MyRepeatable {
}
//注解容器
public @interface MyRepeatables {
MyRepeatable[] value();
}
public class Annotation {
@MyRepeatable
@MyRepeatable
private String a;
@MyRetention
public static void main(String[] args) throws NoSuchFieldException, NoSuchMethodException {
}
}
还是要使用到注解容器,存储多个注解,我们在使用不需要去管注解容器,方便我们操作
Java自带的标准注解
Java自带的标准注解有很多,我就介绍常用的几种
@Override
最常用的注解,来进行方法的重写检查
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
编译后会被编译器丢弃
@SuppressWarnings
告诉编译器忽略此处代码产生的警告
关于注解可以阅读下面这篇文章
Java魔法堂:注解用法详解——@SuppressWarnings
还有更多的 Java 自带的标准注解,请自行探索
自定义注解
自定义注解就是用户自己定义的注解,现在你会看上面元注解样式阶段可以看到很多自定义注解
使用 @interface 自定义注解,自动实现 java.lang.annotation.Annotation 接口,@interface用来声明一个注解,其中的每一个方法实际上是声明了一个配置参数。方法的名称就是参数的名称,返回值类型就是参数的类型(返回值类型只能是基本类型、Class、String、enum)。可以通过default来声明参数的默认值。
基本格式
//一些元注解
.....
public @interface 注解名 {
类型 配置名() [defult 默认值]
......
}
案例(参数校验-注解反射版)
这个案例我仅使用原生的java注解和反射,其中涉及到反射的部分如果目前没学过可不用关注,只是通过一个案例来给你介绍注解在工程中的作用,可以等你学完反射后再来了解本案例。
我们先来定义两个自定义注解
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AgeCheck {
String value() default "数据不合法";
int max() default 100;
int min() default 0;
}
@Target({ElementType.METHOD,ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface NotNull {
String value() default "数据不能为空";
}
再来定义一个实体类
public class Student {
@NotNull(value = "姓名不能为空")
private String name;
@AgeCheck
@NotNull("年龄不能为空")
private Integer age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
}
创建一个测试类(后期我会用代理进行改进)
public class Annotation {
public static void main(String[] args) throws NoSuchFieldException, NoSuchMethodException, IllegalAccessException {
Student student = new Student();
student.setAge(-1);
Class<? extends Student> studentClass = student.getClass();
for (Field field : studentClass.getDeclaredFields()) {
//判断字段是否被 @NotNull 标记
if (field.isAnnotationPresent(NotNull.class)){
//获取字段上标记的 @NotNull 注解实例
NotNull annotation = field.getAnnotation(NotNull.class);
//将 private 修饰的字段设置为 可见
field.setAccessible(true);
Object o = field.get(student);
if (o==null){
System.out.println(annotation.value());
//抛出异常
continue;
}
}
if (field.isAnnotationPresent(AgeCheck.class)){
AgeCheck annotation = field.getAnnotation(AgeCheck.class);
field.setAccessible(true);
Integer i = (Integer)field.get(student);
if(i<annotation.min()||i>annotation.max()){
System.out.println(annotation.value());
}
}
}
}
}
本文主要讲解了 Java注解,以及一个案例来实现参数校验,本文的标题虽然是 Java注解一篇吃透,但还需要自己前身体验才能有所效果,实践出真知。