注解,可以看作是对 一个 类/方法 的一个扩展的模版,每个 类/方法 按照注解类中的规则,来为 类/方法 注解不同的参数,在用到的地方可以得到不同的 类/方法 中注解的各种参数与值

什么是注解

  • 注解是JDK5开始引用的技术

  • 注解说白了就是代码里的特殊标志,这些标志可以在编译,类加载,运行时被读取,并执行相应的处理,以便于其他工具补充信息或者进行部署

  • 注解的格式:

    注解是以@+注解名在代码中存在的,比如:@Value,@Data…,同时注解还可以添加参数,如@suppressWarnings(“unchecked”)

  • 注解的使用范围

    取值 注解使用范围
    METHOD 可用于方法上
    TYPE 可用于类或者接口上
    ANNOTATION_TYPE 可用于注解类型上(被@interface修饰的类型)
    CONSTRUCTOR 可用于构造方法上
    FIELD 可用于域上
    LOCAL_VARIABLE 可用于局部变量上
    PACKAGE 用于记录java文件的package信息
    PARAMETER 可用于参数上

基本注解使用

以下三个注解是JDK提供的内置注解,当然不止这些,这里用三个注解来讲解如何使用他们的

@Override

作用:检查该方法是否是重写方法。

就好比这里有个User类 继承了Object类,这里重写父类的toString()方法

public class User extends Object{
    @Override
    public String toString() {
        return super.toString();
    }
}

上面这样完全没有问题,但是我们一个不小心就把toString的写成了tostring那么就会出现以下情况:

  • IDEA会报错,提示:方法未从其超类重写方法
  • 运行该方法的时候会报错,提示:方法不会覆盖或实现超类型的方法

image-1660294693231

所以这里就印证了上面关于@Override的说明,避免了出现低级错误,没错,就是这么个功能。

@Deprecated

作用: 标记过时方法。如果使用该方法,会报编译警告。(删除线,这个见了不少了吧)。

image-1660294709289

@SuppressWarnings

作用:指示编译器去忽略注解中声明的警告。

image-1660294716648
上面的代码,通常在一个方法中我们定义了一个变量但是没有使用它,在IDEA中就会显示成灰色,而且会有相应提示,下面我们使用注解来看下是什么效果

image-1660294729371
我们发现使用该注解以后,IDEA给提示的警告没有了,字体也变成了黑色。

元注解

元注解是负责对其它注解进行说明的注解,自定义注解时可以使用元注解。Java 5 定义了 4 个注解,分别是 @Documented、@Target、@Retention 和 @Inherited。Java 8 又增加了 @Repeatable 和 @Native 两个注解。这些注解都可以在 java.lang.annotation 包中找到。下面主要介绍每个元注解的作用及使用。

@Target

作用:该注解用来指定一个注解的使用范围,即被@Target修饰的注解可以用在什么地方。

@Target只能被用来标注“Annotation类型”,而且它被用来指定Annotation的ElementType属性。

image-1660294743227

这里我们自定义了一个@MeiShaYong注解,通过给@Target注解的value属性传了俩参数,指定了咱的注解可以用在方法和类上面,为了测试,我们在成员变量上也加了该注解,发现IDE高亮警告,并提示:‘@MeiShaYong’ 不适用于字段。

我们可以查看@Target注解的value属性的枚举类ElementType,看看里面具体有哪些范围可供我们使用:

名称 说明
TYPE 类、接口(包括注释类型)或枚举声明
FIELD 字段声明(包括枚举常量)
METHOD 方法声明
PARAMETER 形参声明
CONSTRUCTOR 构造方法声明
LOCAL_VARIABLE 局部变量声明
ANNOTATION_TYPE 注释类型声明
PACKAGE 包声明
TYPE_PARAMETER 用来标注类型参数
// 如下是该注解的使用例子

@Target(ElementType.TYPE_PARAMETER)
public @interface TypeParameterAnnotation {}

public class TypeParameterClass<@TypeParameterAnnotation T> {
public <@TypeParameterAnnotation U> T test(T t) {
return null;
}
}
TYPE_USE 能标注任何类型名称,包括上面的类型参数

@Target(ElementType.TYPE_USE)
public @interface TypeUseAnnotation {}


public static @TypeUseAnnotation class TypeUseClass<@TypeUseAnnotation T> extends @TypeUseAnnotation Object {
public void test(@TypeUseAnnotation T t) throws @TypeUseAnnotation Exception {

}
}

// 如下注解的使用都是合法的
public static void main(String[] args) throws Exception {
TypeUseClass<@TypeUseAnnotation String> typeUseClass = new @TypeUseAnnotation TypeUseClass<>();
typeUseClass.test(“”);
List<@TypeUseAnnotation Comparable> list1 = new ArrayList<>();
List<? extends Comparable> list2 = new ArrayList<@TypeUseAnnotation Comparable>();
@TypeUseAnnotation String text = (@TypeUseAnnotation String)new Object();
java.util. @TypeUseAnnotation Scanner console = new java.util.@TypeUseAnnotation Scanner(System.in);
}

@Retention

作用:标识这个注解怎么保存,是只在代码中,还是编入class文件中,或者是在运行时可以通过反射访问。

@Retention只能被用来标注“Annotation类型”,而且它被用来指定Annotation的RetentionPolicy属性。

我们可以查看@Target注解的value属性的枚举类RetentionPolicy,查看有哪些方式保存

名称 说明
SOURCE 若 Annotation 的类型为 SOURCE,则意味着:Annotation 仅存在于编译器处理期间,编译器处理完之后,该 Annotation 就没用了。 例如," @Override" 标志就是一个 Annotation。当它修饰一个方法的时候,就意味着该方法覆盖父类的方法;并且在编译期间会进行语法检查!编译器处理完后,“@Override” 就没有任何作用了。
CLASS 编译器将Annotation存储于类对应的.class文件中,即在 class 文件中有效(即 class 保留)。默认行为
RUNTIME 编译器将Annotation存储于class文件中,并且可由JVM读入,即在运行时有效(即运行时保留)

生命周期大小排序为 SOURCE < CLASS < RUNTIME,前者能使用的地方后者一定也能使用。如果需要在运行时去动态获取注解信息,那只能用 RUNTIME 注解;如果要在编译时进行一些预处理操作,比如生成一些辅助代码(如 代码自动生成),就用 CLASS 注解;如果只是做一些检查性的操作,比如 @Override 和 @SuppressWarnings,则可选用 SOURCE 注解。

//@Retention(RetentionPolicy.SOURCE)    如果只是做一些检查性的操作,比如 @Override 和 @SuppressWarnings,则可选用 SOURCE 注解
//@Retention(RetentionPolicy.CLASS)     如果要在编译时进行一些预处理操作,比如生成一些辅助代码(如 代码自动生成),就用 CLASS 注解
@Retention(RetentionPolicy.RUNTIME)//   如果需要在运行时去动态获取注解信息,那只能用 RUNTIME 注解
@interface RetentionAno{}

@Documented

作用:用 @Documented 注解修饰的注解类会被 JavaDoc 工具提取成文档。默认情况下,JavaDoc 是不包括注解的,但如果声明注解时指定了 @Documented,就会被 JavaDoc 之类的工具处理,所以注解类型信息就会被包括在生成的帮助文档中。

定义 注解时,@Documented 可有可无;若没有定义,则 注解不会出现在 javadoc 中。

/**
 * 测试文档注解1
 */
@DocumentedAno
public class DocumentedAnnotation {

    /**
     * 测试文档注解2
     */
    @DocumentedAno
    public void test(){}

}

@Target({ElementType.TYPE,ElementType.METHOD})
@Documented
@interface DocumentedAno{}

以下使用javadoc工具来生成java文档

执行以下命令:javadoc -d doc .\DocumentedAnnotation.java -charset utf-8 -encoding utf-8

PS D:\LEO\个人项目\study\annotation\src\main\java> javadoc -d doc .\DocumentedAnnotation.java -charset utf-8 -encoding utf-8
正在加载源文件.\DocumentedAnnotation.java...
正在构造 Javadoc 信息...
正在创建目标目录: "doc\"
标准 Doclet 版本 1.8.0_301
正在构建所有程序包和类的树...
正在生成doc\DocumentedAnnotation.html...
正在生成doc\package-frame.html...
正在生成doc\package-summary.html...
正在生成doc\package-tree.html...
正在生成doc\constant-values.html...
正在构建所有程序包和类的索引...
正在生成doc\overview-tree.html...
正在生成doc\index-all.html...
正在生成doc\deprecated-list.html...
正在构建所有类的索引...
正在生成doc\allclasses-frame.html...
正在生成doc\allclasses-noframe.html...
正在生成doc\index.html...
正在生成doc\help-doc.html...

点开我们生成好的文档,如下:

image-1660294861431

@Inherited

作用:@Inherited 是一个标记注解,用来指定该注解可以被继承。使用 @Inherited 注解的 Class 类,表示这个注解可以被用于该 Class 类的子类。就是说如果某个类使用了被 @Inherited 修饰的注解,则其子类将自动具有该注解。

@Inherited只能被用来标注“Annotation类型”,它所标注的Annotation具有继承性。

下面我们来测试一下该注解:

@InheritedAnno
public class A {
    public static void main(String[] args) {
        System.out.println(A.class.getAnnotation(InheritedAnno.class));
        System.out.println(B.class.getAnnotation(InheritedAnno.class));
        System.out.println(C.class.getAnnotation(InheritedAnno.class));
        System.out.println(D.class.getAnnotation(InheritedAnno.class));
    }
}

class B extends A{}
class C extends B{}
class D {}

@Inherited
@Target(value = ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@interface InheritedAnno {}

输出如下:

@InheritedAnno()
@InheritedAnno()
@InheritedAnno()
null

@Repeatable

@Repeatable 注解是 Java 8 新增加的,它允许在相同的程序元素中重复注解,在需要对同一种注解多次使用时,往往需要借助 @Repeatable 注解。Java 8 版本以前,同一个程序元素前最多只能有一个相同类型的注解,如果需要在同一个元素前使用多个相同类型的注解,则必须使用注解 “容器”。

比如有个需求,要求给一个方法通过注解的方式设置多角色,其代码实现如下:

Java 8 之前的做法

public @interface Role {
    String roleName();
}
public @interface Roles {
    Role[] roles();
}
public class RolesTest {
    @Roles(roles = {@Role(roleName = "role1"), @Role(roleName = "role2")})
    public String doString(){
        return "具备有多个角色权限";
    }
}

Java 8 之后增加了重复注解,使用方式如下

public @interface Roles {
    Role[] value();
}
@Repeatable(Roles.class)
public @interface Role {
    String roleName();
}
public class RolesTest {

    @Role(roleName = "role2")
    @Role(roleName = "role1")
    public String doString(){
        return "具备有多个角色权限";
    }
}

@Native

使用 @Native 注解修饰成员变量,则表示这个变量可以被本地代码引用,常常被代码生成工具使用。

一个很好的例子是serialVersionUIDjava 8 类的 @native。

这样,我们确保 java 程序的扩展能够轻松引用串行版本的 uid,从而确保我们引用正确的类。

自定义注解

定义方式:

  • 使用@interface 注解名称 {} 的格式定义注解
  • 要使定义注解生效,最少需要定义这俩元注解,@Target和@Retention
  • 定义的注解可以有多个入参,同时每个参数都可以设定默认值
  • 不能使用public关键字修饰注解
  • 跟类不同的是,在注解的作用域中的定义的age(),name()…我们称之为参数,而不是类中的方法
public class CusAnnotation {
    @User(age = 28,name = "lyq",favorite = {"play","run"})
    public void testAno(){}
}

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@interface User {
    int age() default 18;
    String name() default "leo";
    String[] favorite() default {"read","write"};
}

上面就定义了一个完整的注解

小技巧:如果定义的注解只有一个参数,那么建议参数名称是value,这样在使用注解的时候就可以直接在参数中输入值了,若定义的参数名不是value,那么在注解中就必须使用定义的参数名来传参了。如下:

@interface User {
    String name();
}

public class CusAnnotation {
    @User(name = "lyq")
    public void foo(){}
}

如果使用value 效果如下:

@interface User {
    String value();
}

public class CusAnnotation {
    @User("lyq")
    public void foo(){}
}

反射机制

Java Reflection(Java 反射)

以下ORACLE官方对反射机制的描述:

Reflection is a feature in the Java programming language. It allows an executing Java program to examine or “introspect” upon itself, and manipulate internal properties of the program. For example, it’s possible for a Java class to obtain the names of all its members and display them.
The ability to examine and manipulate a Java class from within itself may not sound like very much, but in other programming languages this feature simply doesn’t exist. For example, there is no way in a Pascal, C, or C++ program to obtain information about the functions defined within that program.

反射是Java编程语言中的一个特性。它允许正在执行的Java程序能够检查自身运行状态信息并能够操纵程序的内部属性。比如反射机制可以使一个类在运行时获得自身所有成员的名字,并且展示出来。这种在程序中检查和操控自身的能力不多见,而且在C/C++中没有这种特性。

反射可以实现功能:

  1. 在运行时获取类自身的相关信息,比如类名,类的修饰符,类的成员信息
  2. 在运行时通过反射,获取类的构造器,创建新的对象
  3. 在运行时通过反射调用执行类的方法。(包括private 修饰的方法)
  4. 在运行时通过反射修改字段值(private 修饰的亦可)
  5. 在运行时通过反射处理注解
  6. 生成动态代理

获取反射对象

获取反射对象的方式有如下几种:

  • Class<User> userClass = User.class;
    
  • Class userClass = new User().getClass();
    
  • Class userClass = Class.forName("cc.leeleo.demo01.User");
    

下面我们来获取反射对象。

public class Reflection {
    public static void main(String[] args) throws ClassNotFoundException {
        Class c1 = Class.forName("cc.leeleo.demo01.User");
        Class c2 = User.class;
        Class c3 = new User().getClass();
        System.out.println(c1.hashCode());
        System.out.println(c2.hashCode());
        System.out.println(c3.hashCode());
    }
}

class User{
    private String name;
    public int age;
    protected String sex;
}

输出:

460141958
460141958
460141958

通过上面的代码我们可以发现,当一个类被实例化以后,那么它只会有一个Class对象存在内存中,而这个Class中就包含了这个类的所有信息。

获取类的名字

获取包名+类名:

System.out.println(clazz.getName());

获取类名:

System.out.println(c1.getSimpleName());

输出:

cc.leeleo.demo01.User
User

获取类的属性

获取指定名称的属性(public权限)

如果获取private和protected权限的属性,则会报错。

Field field = c1.getField("age");
获取指定名称的属性(所有权限)

可以获取private、protected、public的属性

Field declaredField = c1.getDeclaredField("sex");
获取所有属性(public权限)
Field[] fields = c1.getFields();
获取所有属性(private、protected、public)

虽说可以获得所有权限的属性,但是它获取不了父类的属性

c1.getDeclaredFields()

获取类的方法

获取当前类指定名称的方法(public权限)

getMethod第一个参数往后都是可变参数,可以传多个

Method method = c1.getMethod("setName",String.class);
获取当前类指定名称的方法(所有权限)

可以获取private、protected、public的方法

Method declaredMethod = c1.getDeclaredMethod("setName",String.class);
获取当前类以及所有父类的方法(public权限)
Method[] methods = c1.getMethods();
获取当前类的所有方法(所有权限)

可以获取当前类的private、protected、public的方法

Method[] declaredMethods = c1.getDeclaredMethods();

获取类的构造器

获取指定参数的构造器(public权限)

getConstructor方法接受可变参数,可传多个

Constructor constructor = c1.getConstructor(String.class);
获取指定参数的构造器(所有权限)

可以获取当前类的private、protected、public的构造方法

 Constructor declaredConstructor = c1.getDeclaredConstructor(String.class);
获取当前类的所有构造器(public权限)
Constructor[] constructors = c1.getConstructors();
获取当前类的所有构造器(所有权限)

可以获取当前类的private、protected、public的构造方法

Constructor[] declaredConstructors = c1.getDeclaredConstructors();

获取方法参数和返回参数类型

获取方法的参数类型
public class Test {
    public static void main(String[] args)  {
        Class clazz = Class.forName("cc.leeleo.demo01.User");
        //获取反射对象方法
        Method paramTypeMethod = clazz.getDeclaredMethod("testParamFun", Map.class, List.class,User.class,String.class);
        //获取方法中的参数类型
        Class[] parameterTypes = paramTypeMethod.getParameterTypes();
        for (Class parameterType : parameterTypes) {
            System.out.println(parameterType);
        }
    }
}

class User{
    public void testParamFun(Map<String,User> userMap, List<User> userList,User user,String name){}
}

输出:

interface java.util.Map
interface java.util.List
class cc.leeleo.demo01.User
class java.lang.String

我们发现 通过反射方法的getParameterTypes()获取到的参数打印出来并不是我们想象中的参数类型,泛型的参数没有很好的展示出来,如果单是获取非泛型的参数类型还是很方便的,可以使用该方法。

获取方法的泛型参数类型
public class Test {
    public static void main(String[] args)  {
        Class clazz = Class.forName("cc.leeleo.demo01.User");
        //获取反射对象方法
        Method paramTypeMethod = clazz.getDeclaredMethod("testParamFun", Map.class, List.class,User.class,String.class);
        //获取反射方法的泛型参数类型
        Type[] genericParameterTypes = paramTypeMethod.getGenericParameterTypes();
        for (int i = 0; i < genericParameterTypes.length; i++) {
            System.out.println("反射方法testParamFun的第"+(i+1)+"个参数为:"+genericParameterTypes[i]);
        }
    }
}

class User{
    public void testParamFun(Map<String,User> userMap, List<User> userList,User user,String name){}
}

输出:

反射方法testParamFun的第1个参数为:java.util.Map<java.lang.String, cc.leeleo.demo01.User>
反射方法testParamFun的第2个参数为:java.util.List<cc.leeleo.demo01.User>
反射方法testParamFun的第3个参数为:class cc.leeleo.demo01.User
反射方法testParamFun的第4个参数为:class java.lang.String

通过对比发现,如果方法附带泛型参数类型,那么 通过getGenericParameterTypes()获取的类型更为直观

获取方法返回值的泛型类型
public class Test {
    public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {
        Class clazz = Class.forName("cc.leeleo.demo01.User");
        //获取反射对象方法
        Method paramTypeMethod = clazz.getDeclaredMethod("testReturnFun");
        //获取方法的泛型返回类型
        Type genericReturnType = paramTypeMethod.getGenericReturnType();
        System.out.println(genericReturnType);
    }
}

class User{
    public List<User> testReturnFun(){}
}

输出:

java.util.List<cc.leeleo.demo01.User>

动态创建对象

动态创建对象的两种方式:

  • 通过反射对象创建

    需要注意:使用反射对象的newInstance()方法来创建对象,需要被反射的对象拥有一个public的无参构造函数。

    其本质是调用无参构造器,使用起来相对受限,因为该方法只能通过无参构造器来实例化对象

    public class Test {
        public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
            Class clazz = Class.forName("cc.leeleo.demo01.User");
    
            User user = (User) clazz.newInstance();
            System.out.println(user);
        }
    }
    
    class User{
        public User(){
    
        }
    }
    
  • 通过获取构造器构造

    通过构造器实例化对象相对方便,使用也不受限,即使没有public的无参构造器也不影响实例化对象。

    public class Test {
        public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {
            Class clazz = Class.forName("cc.leeleo.demo01.User");
    
            Constructor constructor = clazz.getConstructor(String.class,int.class,boolean.class);
            User user = (User) constructor.newInstance("张三",18,true);
        }
    }
    
    class User{
        public User(String name,int age,boolean sex){
        }
    }
    

动态调用对象方法

通过上面的方法我们已经可以获取到一个对象了,当然我们也可以调用其内部的public方法,如果我们想要调用其内部的私有方法呢?

下面我们通过反射来完成对象的私有方法调用:

public class Test {
    public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {
        //1.获取反射对象
        Class clazz = Class.forName("cc.leeleo.demo01.User");
		//2.获取构造方法
        Constructor constructor = clazz.getConstructor(String.class,int.class,boolean.class);
        //3.实例化对象
        User user = (User) constructor.newInstance("张三",18,true);
        //4.获取私有方法
        Method write = clazz.getDeclaredMethod("write", String.class);
        //5.忽略权限检查,使得我们可以调用private权限的方法,true是关闭,false是打开(默认)
        write.setAccessible(true);
        //6.执行私有方法
        write.invoke(user,"去执行对象的私有方法...");
    }
}

@Data
class User{
    private String name;
    private int age;
    private boolean sex;

    private void write(String txt){
        System.out.println(txt);
    }

    public User(String name, int age, boolean sex) {
        this.name = name;
        this.age = age;
        this.sex = sex;
    }
}

通过反射获取注解

下面自定义两个注解,给实体类User增加该注解,最后通过反射的方式来获取注解的值

package cc.leeleo.demo01;

import java.lang.annotation.*;
import java.lang.reflect.Field;

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

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@interface Column{
    String name() default "";
    String type() default "varchar";
    int length() default 255;
}

@Table("db_user")
class User{
    @Column(name = "id",type = "int",length = 19)
    private int id;
    @Column(name = "name",length = 50)
    private String name;
}

public class Test {
    public static void main(String[] args) throws ClassNotFoundException{
        //1.获取反射对象
        Class clazz = Class.forName("cc.leeleo.demo01.User");
        //2.获取类的注解对象
        Table table = (Table)clazz.getDeclaredAnnotation(Table.class);
        System.out.println("表名:"+table.value());
        //3.获取所有字段
        Field[] fields = clazz.getDeclaredFields();
        for (Field field : fields) {
            //4.获取字段注解对象
            Column column = field.getAnnotation(Column.class);
            System.out.println(field.getName()+"的字段名:"+column.name());
            System.out.println(field.getName()+"的类型:"+column.type());
            System.out.println(field.getName()+"的字段长度"+column.length());
        }
    }
}