反射-注解-类加载器知识

反射知识

概念

反射机制就是将类的各个组成部分(属性,方法,构造器)封装为其他对象

  • Class
  • Method
  • Constructor

相关的核心类

  • java.lang.Class(这个就是类的对象)
  • java.lang.reflect.Constructor(构造器)
  • java.lang.reflect.Method(方法)
  • java.lang.reflect.Modifier(修饰词)

Class对象的三种获取方法

Java代码在计算机中经历的三个阶段

图片.png
注意这个源代码阶段并不是我们的.java文件而是我们的class文件
所以我们知道有三个阶段,分别是源码阶段,类对象阶段,和运行时阶段。源码阶段的源码肯定是全局唯一存在的。那么Class对象是用这个源码来用ClassLoader进行加载的,所以应该也是全局唯一的,但是后面创建对象用的new,new 出来的对象肯定就不是全局唯一的了。

为什么我们需要获取到我们的Class对象

因为我们之前说过,我们对于反射,我们是需要获取到类对象的属性方法和构造器然后我们获取到了之后就可以很容易的去使用他们。所以我们需要获取Class对象。

Class对象的三种获取方式

Class对象获取的三种方式分别对应了前面我们说过的Java代码在计算机中经历的三个阶段。源代码阶段,Class对象阶段,运行时阶段。我们直接来一个代码来演示一下。
首先我们先创建一个类Person

package cn.cuit.bo.entity;

public class Person {
    private String name;
    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 Person(){
        System.out.println("构造方法执行!");
    }
}

然后我们现在直接来一个案例等下解释一波

package cn.cuit.bo;

import cn.cuit.bo.entity.Person;

public class Test {
    public static void main(String[] args) throws ClassNotFoundException {
        // 源代码阶段获取类加载器
        Class<?> c1 = Class.forName("cn.cuit.bo.entity.Person");
        System.out.println(c1.hashCode());
        // 类对象阶段加载
        Class<Person> c2 = Person.class;
        System.out.println(c2.hashCode());
        // 运行时阶段加载
        Person person = new Person();
        Class<? extends Person> c3 = person.getClass();
        System.out.println(c3.hashCode());
    }
}

首先我们来看看代码啊,首先我们第一个阶段,我们在这里写的时候并没有引入这个包,或者说这个类,我们完全就不知道这个类是哪一个,我们就直接用了Class.forName也就是我们通过这个类的包路径来找到源代码,并且加载进内存然后转化为一个对象。然后看第二阶段,这个时候我们用到了这个类对象,也就是类名的哪个Person对象然后.class,这个时候其实我们是需要导包的,也就是我们知道了我们正在操作的那个类是哪一个,然后最后一个运行时阶段加载,很简单我们的对象有一个方法,.getClass()方法可以很容易获得对象的类。
然后我们来看看运行的结果
图片.png
我们可以看到都获取成功了,并且无论在哪一个阶段进行获取,我们获得到的Class对象的hashCode都是一样的。所以这也证明了Class对象贯穿整个程序周期,是唯一存在的。

Class相关方法详解

class的功能

加载功能【把字节码文件加载到内存中】
  • Class.forName(String classPath)
获取功能

获取成员变量对象

  • getField(String name) 根据属性名称获取非私有的属性
  • getFields() 获取当前class里面的所有非私有的属性
  • getDeclaredField(String name)根据属性名称获取所有属性对象【忽略修饰符】
  • getDeclaredFields() 获取当前class的所有属性对象【忽略修饰符】
    获取构造方法
  • getConstructor(Class<?>… parameterTypes) 根据参数类型和个数获取得到对应的非私有构造方法,参数用来标明不同的构造方法
  • getConstructors()获取所有的非私有的构造方法
  • getDeclaredConstructor(Class<?>… parameterTypes) 根据参数类型获取到对应的构造方法【忽略修饰符】
  • getDeclaredConstructors()获取当前Class的所有构造方法【忽略修饰符】
    获取成员方法
  • getMethod(String name, Class<?>… parameterTypes) 获取到名称为name,参数类型为parameterTypes的非私有的成员方法。
  • getMethods()得到所有的非私有的成员方法
  • getDeclaredMethod(String name, Class<?>… parameterTypes) 获取名称为name参数列表为parameterTypes的方法对象
  • getDeclaredMethods()获取到所有的方法列表。
    获取类名
  • getName()返回的是这个类的全限定类名==(包名 类名)==
  • getSimpleName()返回的是这个类的类名
    创建对象
  • newInstance()方法可以创建一个这个类的对象实例,但是要求必须这个类有一个空参的public的方法才可以。
    加载资源
  • class对象有一个getClassLoader()方法返回一个类加载器,然后类加载器中有一个方法getResourceAsStream(String name)用来返回一个流对象。

Constructor类里面的相关方法详解

方法名称用法
newInstance这个方法调用就类似我们拿到了这个构造器,然后我们执行这个构造器初始化一个对象出来一样。记住这个方法后面的括号里面可以加上参数,如果获取到的构造器是一个有参数的构造器,那么就必须加上构造器里面的参数才可以正常调用newInstance的方法
setAccessiable(bool accessable)这个方法用来设置是否打破权限进行调用,如果我们现在利用getDeclaredConstructor方法获取到一个非公有的构造方法,如果我们立马对其进行调用,那么会失败,因为权限是不够的。所以我们需要设置是否打破权限进行调用,那么我们设置了true之后那么就可以进行调用了,所以对于私有的构造器我们可以利用反射机制进行调用
getModifiers()该方法返回的是构造器的权限访问字,返回的是int类型,返回的数字对应的权限如下private 2 protected 4 public 1 无修饰符 0
getParameterCount返回构造器的参数个数
getParameterTypes返回构造器参数类型数组

Field类里面的相关方法详解

相关方法

方法名称用处
getName获取属性名称
getModifiers获取当前属性的访问权限返回值也是int,和前面的一样
getType()获取属性的类型返回值是一个Class对象
set(Object obj, Object val)给对象obj的这个属性赋值为val,使用这个的时候权限不足会抛出异常,所以需要配合是否打破权限进行
setAccessible(bool accessible)是否打破权限访问

Method里面的方法详解

相关方法

方法定义方法使用
getModifiers获取当前方法的返回权限【int】
getName()得到方法名称
getParameterCount()得到方法的参数个数
getParameterTypes()按照顺序获取方法的参数类型,返回Class的数组
getReturnType()得到返回值的类型
setAccessible(boolean flag)是否打破访问权限
invoke(Object obj, Object… args)执行obj对象中的这个方法,并且如果有参数需要传入这个参数

我们可以思考一下,为什么我们的方法重写里面返回值没有被纳入考虑范围呢?想一下,因为我们利用反射的时候只需要方法名称和参数就可以进行一个调用了,也就确定了一个方法,如果要执行这个方法和返回值压根不沾边,所以返回值没有纳入。

注解

简单理解

注解就是对应用程序的某一个部分的特殊说明,这个说明只针对关注这个说明的程序,如果其他程序不关注这个说明,那么注解对于这个程序来说是无效的。可以理解为一个标签

注解的语法

申明一个注解

package cn.cuit.bo.annotation;

public @interface TestAnnotation {
}

元注解

解释

作用在注解上面的注解就是元注解

元注解种类

@Target

标记这个注解的作用目标

默认来说注解都是可以加在任何的地方的,但是如果想让注解只能加到某些特殊的地方那么就需要用到target了。
@Target(value = {})
这里面的参数是ElementType的枚举类型,常用的有
图片.png
这几种

@Rentention

Retention的英文意思是保留期的意思。当@Retention应用到一个注解上的时候,它解释说明这个注解的过期时间
过期为

名称时间
RetentionPolicy.SOURCE只有在源码阶段保留,其他时候被丢弃
RetentionPolicy.CLASS注解制备保留到编译进行的时候,不会被加载到JVM中
RetentionPolicy.RUNTIME注解可以保留到程序运行的时候,会被加载到JVM中
@Documented

顾名思义,这个元注解是和文档有关系的,用来生成文档的。

@Inherited

是继承的意思,但是不是注解可以被继承的意思,意思是,注解到了一个类上,如果这个类有子类,那么子类也有注解。

@Repeatable

用来标记一个注解是否可以多次出现

举一个例子
图片.png
可以看到上面有一个Person注解上面申明了一个@Repeatable注解并且value是Persons这个注解的字节码,这个表示我们可以看到首先Person这个注解可以出现多次,就像下面的Test类上面注释的部分,我可以写多个@Person注解,或者我可以在Persons里面定义一个person注解数组,这样我也可以写一个@Persons注解里面写多个Person注解。这个persons注解一般叫做容器注解,里面放注解并且里面自己也是一个注解。

注解属性说明、

  • 注解属性的申明是 --> 属性类型 属性名() [default 默认值]; 默认值可以不加
  • 注解属性的类型只能是基本数据类型,String,注解类型这三种。
  • 注解属性如果没有默认值,那么在加注解的时候必须要进行一个赋值,不然会报错。
  • 注解属性如果是一个value那么可以不加注解的名称可以直接填值。

提取注解

前面说过,我们写了注解,注解是一个标签,只对对这个标签感兴趣的代码才有效,所以我们其实还需要写一些代码来提取这个标签,获取标签里面的内容才可以让这个注解产生效果。

我们的注解可以加载类上,属性上,构造器上,方法上,或者方法参数上,我们写一个小小的demo来看看
首先我们定义一个注解,因为没有加上@Target元注解,所以这个注解是可以加到任何地方的。

package cn.cuit.bo.annotation;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@Retention(RetentionPolicy.RUNTIME)
public @interface TestAnnotation {
    String value();
}

然后我们写一个Student类,在Student类上我们在各个地方加上注解

package cn.cuit.bo.entity;

import cn.cuit.bo.annotation.TestAnnotation;

@TestAnnotation("我是加到类上的注解")
public class Student {
    @TestAnnotation("我是加到属性上的注解")
    String name;

    @TestAnnotation("我是加到构造方法上的注解")
    public Student(){}

    @TestAnnotation("我是加到方法上的注解")
    public void sayHello(@TestAnnotation("我是加到方法参数上的注解") String objName, String word){ System.out.println(objName   ":"   word); }
}

然后我们来一段测试demo

package cn.cuit.bo;

import cn.cuit.bo.annotation.TestAnnotation;

import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;

public class Test{
    public static void main(String[] args) throws Exception {
        // 获取到Student类的Class对象
        Class<?> c1 = Class.forName("cn.cuit.bo.entity.Student");
        /*获取类上的注解*/
        TestAnnotation anno1 = c1.getDeclaredAnnotation(TestAnnotation.class);
        System.out.println(anno1.value());
        // Annotation[] anno1s = c1.getDeclaredAnnotations(); 获取这个类上的所有注解

        /*获取构造器上的注解*/
        Constructor<?> constructor = c1.getDeclaredConstructor();
        TestAnnotation anno2 = constructor.getDeclaredAnnotation(TestAnnotation.class);
        System.out.println(anno2.value());
        // constructor.getDeclaredAnnotations(); 获取这个类的构造器上的所有注解

        /*获取属性上的注解*/
        Field f = c1.getDeclaredField("name");
        TestAnnotation anno3 = f.getDeclaredAnnotation(TestAnnotation.class);
        System.out.println(anno3.value());
        // Annotation[] declaredAnnotations = f.getDeclaredAnnotations(); 获取这个属性上的所有注解

        /*获取方法上的注解*/
        Method sayHello = c1.getDeclaredMethod("sayHello", String.class, String.class);
        TestAnnotation anno4 = sayHello.getDeclaredAnnotation(TestAnnotation.class);
        System.out.println(anno4.value());
        // sayHello.getDeclaredAnnotations(); 获取这个方法上的所有注解;

        /*获取方法参数上的注解*/
        Annotation[][] annoss = sayHello.getParameterAnnotations();
        // 遍历
        for(Annotation[] anns : annoss) {
            for(Annotation ann : anns){
                if (ann instanceof TestAnnotation) {
                    TestAnnotation ta = (TestAnnotation)ann;
                    System.out.println(ta.value());
                }
            }
        }
    }
}

然后我们来观察一下结果
image.png
很好啊,我们可以看到结果也是非常的符合我们的预期,所有的注解都被获取到了里面的值我们也得到了。这里顺便说一个值得注意的事情,我们的注解一定一定要设置作用时间,不然等我们获取的时候它已经超过了过期时间了。

类加载器

类加载器的作用

用来加载字节码文件

类加载器的获取方法

Class --> getClassLoader()
ClassLoader类 --> ClassLoader.getSystemClassLoader()

类加载器中的重要方法

方法名称作用
getParent()得到父加载器
Class<?> loadClass(String name)根据类的完全限定名获取字节码文件
Class<?> findClass(String name)一句类的完全限定名来查找内存里面的class对象
findLoadedClass查找名称为name的,已经被加载过的类,返回的结果是Class
defineClass(String name, byte[] b, int off, int len)这个是用来加载类的一个方法,我们的JVM加载类的时候可以根据完全限定名来查找文件然后利用io流加载为一个byte数组,然后使用这个defineClass方法把这个byte信息注册成为一个类,并且加载到内存中
resolveClass(Class<?> c)链接指定的Java类

类加载过程详解

JVM将类加载过程分为三个步骤:装载(Load),链接(Link)和初始化(initialize)

装载过程

查找并加载类的二进制数据

链接

验证

确保加载类信息符合JVM规范,没有安全方面的问题

准备

为类的静态变量分类内存,并将其初始化为默认值

解析

把虚拟机常量池中的符号引用转换为直接引用

初始化

为类的静态变量赋予正确的初始值

解析这一部分需要说明,在java中,虚拟机会为每个加载的类维护一个常量池【不同字符串常量池,这个常量池知识该类的字面值(例如类名,方法名)和符号引用的有序集合。而字符串常量池,是整个JVM共享的】这些符号(例如int a = 5,中的a)就是符号引用,解析过程就是转换为堆中的对象的相对地址

JVM中类加载器的树状层次结构

Java中的类加载器大致可以分成两类,一类是系统提供的,另外一类则是由Java应用开发人员编写的。

引导类加载器(bootstrap class loader)

它用来加载Java的核心库(jre/lib/rt.jar), 是用原生C 代码来实现的,并不继承自java.lang.ClassLoader类。
加载扩展类和应用程序类加载器,并置顶他们的父类加载器,在java中获取不到。

扩展类加载器(extensions class loader)

它用来加载java的扩展库(jre/ext/*.jar)Java虚拟机的实现会提供一个扩展库目录。该类加载器再次目录里面查找并加载Java类。

系统类加载器(system class loader)

它根据Java应用的类路径(CLASSPATH)来加载Java类。一般来说,Java应用的类都是由他来加载完成的。可以通过ClassLoader.getSystemClassLoader()来获取。我们自己编写的类的加载器就是这个系统类加载器。

自定义类加载器(custom class loader)

除了系统提供的类加载器以外,开发人员可以通过继承java.lang.ClassLoader类的方式实现自己的类加载器,以满足一些特殊的需求。

加载器的树状结构

其实上面的前面三种加载器都是有树状的一个结构的,有父子关系。我们可以来试试看。写一个小demo

package cn.cuit.bo;


public class Test{
    public static void main(String[] args) throws Exception {
        ClassLoader c1 = Test.class.getClassLoader();

        ClassLoader c2 = c1.getParent();

        ClassLoader c3 = c2.getParent();

        System.out.println(c1);
        System.out.println(c2);
        System.out.println(c3);
    }
}

好的首先我们获取了当前这个我们自己写的Test类的加载器,然后可以通过getParent方法来获取这个加载器的父加载器,然后我们一共往上查找了三次
我们来看下结果
image.png
可以看到我们自己的Test类的加载器是AppClassLoader,然后它的父加载器就是扩展加载器PlatFormClassLoader最后我们可以看到再往上就是null了,这个原因其实就是再往上就是引导类加载器了,这个时候获取不到的所以是null

双亲委派机制

我们的类加载器,首先碰到一个类之后,如果需要用到那么就会让当前的类的加载器进行加载,一般来说,当前类加载器会直接把这个任务抛给上一级,一直往上面抛,最后到达最后的类加载器,这个时候最顶部的类加载器又会往下分任务,如果下面的加载器已经加载了这个类的话那么就不用再去加载这个类了。这就是双亲委派机制。

作用

我们可以来试试一个东西,一个小小的demo

image.png
可以看到这个demo我写了一个和String类一摸一样的类,包名和类名都是一样的,这个时候如果我们运行这个入口函数,那么会出现什么问题呢?
image.png
可以看到,它说没有main方法,但是图片上面明显有main方法,这是为什么呢?其实是这个样子的,首先我们需要运行这个方法,那么我们自己写的String方法是必须要进入到内存中的,然后就需要类加载器去进行一个加载,然后一直网上抛这个任务,最后顶部的加载器遇到之后,发现自己已经加载了这个String类了(加载了官方的String类)然后就告诉你不需要加载了可以直接运行,然后就调用官方的String中的main入口函数,这样的话原本的String中是没有这个入口函数的,所以就出现了上面的那种情况。
所以我们可以知道其实双亲委派机制可以保护我们的类不要被后面的类干扰,保护核心内部的Java类。

自定义加载器

我们只需要用一个类去继承ClassLoader就可以实现一个加载器了,我们之前说过,将一个类加载到内存中我们需要用到类加载器中的一个方法,defineClass

我们直接来看一个小案例,写了一个自定义的类加载器

package cn.cuit.bo.classloader;

import java.io.*;

public class MyFileClassLoader extends ClassLoader{

    public MyFileClassLoader(String rootPath){
        this.rootPath = rootPath;
    }
    private String rootPath;

    // 获取类全限定名找到字节码文件变为字节数组
    private byte[] getData(String name) throws ClassNotFoundException {
        ByteArrayOutputStream out = null;
        byte[] data = null;
        String path = rootPath   name.replace(".", File.separator)   ".class";
        BufferedInputStream bis = null;
        try{
            bis = new BufferedInputStream(new FileInputStream(path));
            byte[] buffer = new byte[1024 * 8];
            out = new ByteArrayOutputStream();
            // 循环读取到 out 里面
            int len = -1;
            while((len = bis.read(buffer)) != -1) {
                out.write(buffer,0,len);
            }
            out.flush();
            data = out.toByteArray();
        }catch (FileNotFoundException e) {
            e.printStackTrace();
            throw new ClassNotFoundException();
        }catch (IOException e){
            e.printStackTrace();
        } finally {
            try {
                if(out != null) out.close();
                if(bis != null) bis.close();
            }catch (Exception e){
                e.printStackTrace();
            }
        }
        return data;
    }


    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        // 判断一下有没有被其他类加载器加载过了
        Class<?> loadedClass = this.findLoadedClass(name);
        if(loadedClass != null) return loadedClass;
        else {// 需要加载
            byte[] data = this.getData(name);
            Class<?> aClass = this.defineClass(name, data, 0, data.length);
            return aClass;
        }
    }
}

然后我们解释一下这个代码,主要重写的方法是findClass这个方法,传入一个类的全限定名我们就可以开始解析了,首先我们可以判断一下这个类是不是已经被别的加载器加载进来了,如果已经加载进来了的话,那么就直接返回就完事了,如果没有那么就需要我们自己去进行加载操作了。我们这个案例是从文件中加载类,这个时候肯定需要提取到文件的路径了,一般来说我们的包名其实就是一个相对路径,所以我们有一个根路径,我们需要知道所以我们定义了一个属性叫做rootPath然后我们写一个构造方法来给这个rootPath传值,这样路径有了我们就可以知道从哪里提取出字节码文件了,前面我们说过我们加载一个类就要用到defineClass方法,这个方法需要的全限定名我们有,还需要一个字节数组,所以我们现在当务之急是获取到这个数组,所以我们又编写了一个方法,传入全限定类名提取出字节数组,这个方法里面,我们先初始化了一个BufferedInputStream对象,然后又创建了一个ByteArrayOutPutStream对象,这个对象我们可以写到这个输出流里面然后获取byte数组,然后循环输出就完事了,然后获取到之后直接返回,然后直接调用findClass就可以了。非常的简单

测试一下

package cn.cuit.bo.classloader;

public class Test{
    public static void main(String[] args) throws Exception {
        MyFileClassLoader myFileClassLoader = new MyFileClassLoader("C:\\Users\\1902001047\\Desktop\\DemoTest\\");
        Class<?> aClass = myFileClassLoader.findClass("cn.cuit.bo.wuhu.HelloJava");
        aClass.newInstance();
        ClassLoader c1 = aClass.getClassLoader();
        ClassLoader c2 = c1.getParent();
        ClassLoader c3 = c2.getParent();
        ClassLoader c4 = c3.getParent();
        System.out.println(c1);
        System.out.println(c2);
        System.out.println(c3);
        System.out.println(c4);
    }
}

操作结果

image.png
首先这个结果确实是是成功加载了这个类,并且我们获取它的类加载器,确实是我们的MyFileClassLoader加载器,这个时候我们继续向上获取加载器,发现它的上面是AppClassLoader,哦!原来我们自己写的类加载器其实是最底层的加载器

泛型擦除测试

原本我们知道,如果我们定义了一个List集合,我们需要加上泛型,如果这个泛型我们是String类型,那么我们尝试去add一个Integer会怎么样呢?

很明显是不可以的,因为泛型是String但是我们非要加上一个Integer这怎么去弄呢?其实还是有办法的,我们可以利用反射机制去擦除我们的泛型。来看一个案例

public static void main(String[] args) throws Exception {
        List<String> strs = new ArrayList<>();
        strs.add("AA");
        strs.add("BB");
        // 获取Class对象
        Class<? extends List> cs = strs.getClass();
        Method add = cs.getMethod("add", Object.class);
        add.invoke(strs,123);
        System.out.println(strs);
    }

因为我们不可以直接进行add操作,但是我们通过反射拿到了这个method,然后传入参数,因为泛型操作一般参数都是Object,只有再编译的时候这个泛型T才会被检测,检测传入的参数是不是T这种类型的,但是我们利用反射机制,其实是再运行阶段才会进行运行,在编译之前没有什么异常的操作。所以通过了编译阶段之后就可以放飞自我了。

本次学习参考Blibli视频
链接:JAVA高级特性_反射_注解_类加载器【雷哥】

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值