Java进阶篇之——反射与注解

Java中的反射与注解,很多人不太明白,这明明是两个不相干的内容,为什么大家总是放到一块去讲述?实际上这他们两个单独使用并没有什么特别的,但是二者结合可以产生超脱认知的程序编写方式。

本文将从三方面讲述反射和注解之间的兄弟情结:

  1. 注解介绍
  2. 反射介绍
  3. 注解式开发

今后的Java架构中将大量的出现注解式开发,它其实没什么令人难以理解的,经过本文的阅读,你将轻而易举的推算出它们的实现。

第一节 注解

注解 Annotation)又称为Java标注,Java中的类、方法、变量、参数和包等都可以被标注。

在编译器生成类文件时,注解可以被嵌入到JVM字节码中。和我们平常的注释不同,通过此法JVM虚拟机可以阅读你标注内容,从而构建代码程序。

所以那些主流框架程序可以通过反射去获取注解内的代码,让框架更加简洁(注解式开发)。

 

Java 10个注解

6个在java.lang中,剩下4个在java.lang.annotation

@override - 检查该方法是否存在重写方法。如果父类或者引用接口中并没有该方法时,会报编译错误。

@Deprecated - 标记过时的方法。如果使用该方法会编译警告

@SupperssWarnings - 指示编译器忽略注解中声明的警告。

@SafeVarargs - Java 7 开始支持,忽略任何使用参数为泛型变量的方法或构造函数调用产生的警告。

@FunctionalInterface - Java 8 开始支持,标记一个匿名函数或者函数接口(我在Java-Lambda篇:函数接口中有提到)

@Repertable - Java 8 开始支持,表示某注解上同一个声明使用多次。

 

剩下四个称之为元注解:

@Retention - 表示这个注解是怎么保存的,是只在代码中,还是注入到Class文件中,或者在运行时可以通过反射访问。

@Target - 标记这个注解应该是那种 Java 成员,允许标注的地点。

@Documented - 标记这些注解是否包含在用户文档。

@Inherited - 标记这个注解是继承于那个注解类(默认 注解并没有继承于任何子类)

 

我们遇到比较多的是@override 表示重写的元注解 和 @Deprecated 表示弃用的元注解,但是今天我们介绍注解式开发最核心的两个元注解:@Target和@Inherited

元注解——首次开发注解使用示范:

1、@Target

首先我们先观察下@Target 注解:

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Target {

    ElementType[] value();
}

它最为核心的就是这个ElementType[] 枚举类型,它表示该注解可以存在于class文件的什么位置(类名上方、方法上方、参数上方),枚举类型中包含了以下几种类型(JDK 17)

名称

地点

来自JDK

TYPE

Type,可以在类、接口、枚举上注解

1.5

FIELD

Field,类的字段常量,也可以在枚举上注解

1.5

METHOD

Method,方法上

1.5

PARAMETER

Parameter,方法的参数上

1.5

CONSTRUCTOR

Constructor,构造函数上

1.5

LOCAL_VARIABLE

Local Variable,方法内的局部变量上

1.5

ANNOTATION_TYPE

Annotation Type,注解上

1.5

PACKAGE

Package,Package-info文件中

1.5

TYPE_PARAMETER

Type Parameter,泛型中类型参数声明

1.8

TYPE_USE

Type Use,泛型中指定具体类型(子类继承)

1.8

MODULE

module,模块上,JDK9模块化新增

9

RECORD_COMPONENT

标注在记录类的组件上

16

这个注解规定了你自定义注解放置的位置。

2. @Retention

这个注解就更好理解了,它代表注解的生命周期

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Retention {
    RetentionPolicy value();
}

名称

地点

来自JDK

SOURCE

注解只保留在源文件,编译完毕后消除注解

1.8

CLASS

注解被保留到class文件,类加载前消除

1.8

RUNTIME

注解在运行时依旧存在,可以被反射获取

1.8

所以我们在进行注解开发时,为了保证JVM虚拟机能够阅读到注解,我们必须规定 @Retention 为RUNTIME

3.@Documented

这个注解没啥好说的,主要帮助生成API文档,项目当中相当重要,但不在我们本文主要演示范围内。

4.自定义注解

我们先定义一个自定义的注解:

@Target({ElementType.METHOD,ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyAnnotation {
    String value() default "";
    int age() default 0;
}

这个注解中第一个元注解@Target中的参数叫做ElementType.METHOD和ElementType.FIELD,这个注解是可以标注在对象方法以及对象常量之上的:

public class Dog {
    private String name;
    @MyAnnotation(age = 10) //标注在常量字段之上
    private int age;
    
    public String getName() {
        return name;
    }
    @MyAnnotation("ABC") //标注在方法之上
    public void setName(String name) {
        this.name = name;
    }
}

注解内部中有一个默认的value() 方法,它允许你使用注解时你可以不填写注解属性的名字,左侧的类型代表你注解的输入类型,而其他非value名的方法代表你在使用注解时你需要指定该属性的名字。

此处我们直接填写一个字符串到注解中,它要求必须输入字符串,我们用来作为名字注入到狗对象属性常量中,而age常量上,我们的注解指定了age方法是一个int数字值。同时你还可以添加default 关键字来设定默认值。

那么仔细研究,尝试的去使用注解,你就可以理解注解的使用方法了。

 2.反射

反射是非常暴力的方式去建立对象

java是动态加载的语言,因为设计者并没有限定Class一定是从磁盘来的,也可能是网络,也可能是压缩包,总之Class是符合规则的二进制代码就行,剩余的建立对象的任务都会交由JVM虚拟机来建立,而且每个Class一定是独一无二的.所以反射最重要的用途就是开发各种通用框架。

那么Class是什么?
答:Class是一个类,小写的class代表这是个类型,大写的Class代表了这个类的名称。

Class文件中到底包含了那些信息?
答:一个类中应该有类名,属性,方法,构造器,修饰符,常量等等。

Class类是一个对象照镜子的结果,对象可以看到自己有哪些属性,方法,构造器,实现了哪些接口等等。

对于每个类而言,Class的类型只会保存一个,并且它其中保存了这个类的所有有关信息,而对象必须由这个类加载器实现。

 

那么我们为什么要使用反射?
1.这个对象,不属于我们磁盘当中,它可能是从网络或者更加奇妙的地方传递来的。
2.可能我们没有这个对象,只有一个全类名。
3.有些信息,我们并不能简简单单的通过"变量"来读取。

通过反射,我们可以得到这个类里面的全部信息,并且动态的间造对象

获取Class对象三种方式:

1.通过类名获取     例:  Class <User> cc = User.class

2.通过对象获取     例:  Class<? extends User> cc = user.getClass();    //继承user的子类都为反射类型。

3.通过全类名获取  例:  Class<?> cc = Class.forName("top.domain.User")  //JDBC开发中常用此方法加载数据库驱动

记住,用这三种方法获取的Class永远的只能获取且仅有的一个,所以这三种方式获取的Class类都是一致的(内存地址相同)

Class类的常用方法:

  1.建立对象

方法名

作用

实例

.istanceof(Object obj)

判断是否属于某个类

apple instanceof Apple ? true : false;

.getName()

返回Class全名(或对象路径)

Wolf.class.getName()

.getSimpleName()

返回Class简略类名

Wolf.class.getName() 

 

  1. 获取类的构造方法(Constructor)

方法名

作用

实例

.getConstructor()

获取类不带参且public权限的构造函数

Constructor<Apple> constructor = Apple.class.getConstructor();

.getConstructors()

获取类全部public权限的构造函数List

Constructor<?>[] constructors = Apple.class.getConstructors();

.getDeclaredConstructor()

暴力获取类不带参构造函数

Constructor<Worlf> declaredConstructor1 =Wolf.class.getDeclaredConstructor();

.getDeclaredConstructors()

暴力获取类全部的构造函数List

Constructor<?>[] declaredConstructors = Wolf.class.getDeclaredConstructors();

构造方法是允许重载的,因此我们可以指定对象的构造方法,例如:

Constructor<Worlf> constructor = Wolf.class.getDeclaredConstructor(String.class, int.class);

  基本上来说,获取一个类的全部构造方法似乎毫无用处,我们并不知道自己想要的构造方法在那里,所以实际上我们都是获取反射类的无参构造方法后建立实例,使用get和set方法填充进去.此时我们构造方法获得了,那么我们就需要使用构造方法建立实例了:

 

2.1 构造方法建立实例对象:

.getConstructor()/.getDeclaredConstructor()

.newInstance(Object ... initargs)

使用构造方法建立对象

Constructor<Wolf> constructor = Wolf.class

                                    .getDeclaredConstructor(String.class, int.class);

Wolf wolf = constructor.newInstance("大狼", 2);

.newInstance(Object ... initargs)

暴力的构造方法建立实例

和上面类似

 

 

3.获取字段常量(Type)

方法名

作用

.getFields()

获取所有

Public 属性的字段常量

clazz.getFields();

.getDeclaredFields()

返回数组类型

以数组的形式返回所有属性字段

Field[] fields = clazz.getDeclaredFields();

Arrays.stream(fields).forEach(System.out::println);

.getField(Str)

获取特定属性

Public 属性字段常量

Field name = clazz.getField("name")

.getDeclareField(Str)

获取特定属性

暴力获取类封装的

Field used = clazz.getDeclareField("name")

对Field类操作,如果常量属性不是Public类型,那么需要设置setAccessible属性:

方法名

作用

.getName()

获取常量名

field.getName()

.getModifiers()

获取修饰符(以数字来表示)

1表示public,4表示protected,16表示default,3表示private

.getType()

获取常量的Class

 

.set(Obj obj,Obj value)

传入对象,修改其属性

Dog objDog= new Dog("dang",2,DogType.XIA);

Field name = clazz.getDeclaredField("dogType");

name.set(objDog,DogType.China);

.setAccessible(boolean);

允许该对象修改非pulice值

name.setAccessible(true);

.setInt/Byte... (Obj obj,Int/Byte... value)

修改对象属性,基本类型特化

 

 

3.获取方法(Method)

方法名

作用

.getMethod(Str name,Class<?>... parameterTypes)

传入方法名,返回值类型,返回特定的方法

clazz.getMethod("setName", String.class);

.getMethods()

获取所有的方法

 

.getDeclaredMethod(Str, ...Types)

暴力获取方法

clazz.getDeclaredMethod("setName", String.class);

.getDeclaredMethods()

暴力获取所有方法

 

对Method类进行操作,如果方法不是Public修饰符,那么需要设置setAccessible属性,来开放权限(有够暴力的)

方法名

作用

.invoke(Obj obj,Obj... args)

应用方法

setName.invoke(dog,"huahua");

.setAccessible(boolean);

允许该对象修改非Pulice值

name.setAccessible(true);

.getReturnType()

取得返回值类型

 

.getModifiers()

获取修饰符(以数字来表示)

1表示public,4表示protected,16表示default,3表示private

 

4.获取注解(Annotation)

注解可以从类、方法、构造器,参数上获取到注解,通过这些注解可以获取到重要的代码信息。

方法名

作用

.getDeclaredAnnotations()

获取所有注解

 

.getAnnotations()

获取所有Pulice注解

 

.getDeclaredAnnotation(Class<A>)

暴力获取指定类的注解

clazz.getDeclaredAnnotation(Controller.class);

.getAnnotation(Class<A>)

获取指定类的注解

clazz.getAnnotation(Controller.class);

 

2.反射式编程

我们学习了注解方法,也学习了反射,现在我们只需要获得类的加载器,就能读取到他所有的方法、注解、参数,你所能想到的所有都能读取到!
注意:我使用了Lombok插件,它使用了注解的方式来对类的方法进行了一系列的补充。这里还有个枚举类 DogType 没有给出
 
1. 注解的构建
@Target表明了注解可以放置的位置,方法、类名以及字段常量上
@Target({ElementType.METHOD,ElementType.FIELD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyAnnotation {
    String value() default "";
    int age() default 0;
    DogType dogType() default DogType.China;
}
2.对象构建
 

先定义了一个枚举类;

public enum DogType {
    China("中华田园犬"),
    English("英国贵族犬");
    private String name;
    DogType(String name){
        this.name = name;
    }
    public String getName(){
        return name;
    }
}
将枚举类插入到对象中
@MyAnnotation("铁蛋")
@Data  //来自于Lombok
@NoArgsConstructor //来自于Lombok
public class Dog {
    private String name;
    @MyAnnotation(age = 2)
    protected int age;
    @MyAnnotation(dogType = DogType.English)
    DogType dogType;
    public String color;
}
 
3.效果实例:
public class demo01 {
    public static void main(String[] args) throws NoSuchFieldException, 
    NoSuchMethodException, 
    InvocationTargetException, 
    InstantiationException, 
    IllegalAccessException {
        Class<Dog> clazz = Dog.class;

        //获取类上方的注解,它是小狗的名字
        MyAnnotation ant = clazz.getDeclaredAnnotation(MyAnnotation.class);
        String name = ant.value();

        //获取类字段常量上方的注解,他是小狗的年龄
        Field fieldAge = clazz.getDeclaredField("age");
        MyAnnotation ant2 = fieldAge.getAnnotation(MyAnnotation.class);
        int age = ant2.age();

        //获取类字段常量上方的注解,他是小狗的国籍
        Field fieldDogType = clazz.getDeclaredField("dogType");
        MyAnnotation ant3 = fieldDogType.getAnnotation(MyAnnotation.class);
        DogType dogType = ant3.dogType();

        Constructor<Dog> constructor = clazz.getConstructor();
        Dog dog = constructor.newInstance();
        //获取构造器,创建对象

        dog.setName(name);
        dog.setAge(age);
        dog.setDogType(dogType);
        //将空值对象注入属性
        
        System.out.println(dog);
        //Dog(name=铁蛋, age=2, dogType=English, color=null)
    }
}

完成这些练习后,相信你也对注解+反射的框架有了初步的了解!

 

posted @ 2022-03-19 17:12  木半夏曦  阅读(217)  评论(0)    收藏  举报