Java学习笔记 14、反射与JDK动态代理

举报
长路 发表于 2022/11/22 21:49:40 2022/11/22
【摘要】 文章目录前言一、认识反射二、认识Class类Class类获取Class实例的四种方式哪些类型可以是class对象?三、反射的方法使用获取构造器、类属性及方法如何看待反射与封装性两个技术?四、类的加载与ClassLoader理解类的加载过程(含例子)ClassLoader理解(各个类加载器)认识各个类加载器ClassLoader双亲委派机制加载properties文件五、获取运行时类的完整结构六、调

@[toc]


前言

学习背景:在学习了ssm框架、springboot之后对java基础进行回炉重造,初学java看的是 尚硅谷_Java零基础教程-java入门必备-适合初学者的全套完整版教程(宋红康主讲) ,现基于以前笔记以及在网上看前辈博客进行梳理总结,对于反射以及jdk动态代理有了更进一步的理解。本篇博客部分图片引用了尚硅谷的视频课件,如有侵权,联系我删除。

博客汇总目录博客目录索引(持续更新)



一、认识反射

反射(reflection):视为动态语言的关键,反射机制允许程序在执行期间借助JDK中提供的Reflection API来取得任何类的内部信息,并能够直接操作任意对象的内部属性及方法,很多框架中都使用到了反射,例如Spring

那么它是如何在运行期间通过反射获取对象及类中的属性呢

  • JVM加载完类之后,在堆内存的方法区中就产生了一个Class类型的对象(一个类只有一个class对象),这个对象包含了完整的类的结构信息,可以通过该类来获取或使用我们想要的属性及方法。

关于反射的API

  • java.lang.Class:表示一个类(表示通用的类)。
  • java.lang.reflect.Method:代表类的方法。
  • java.lang.reflect.Field:代表类的成员变量。
  • java.lang.reflect.Constructor:代表类的构造器。


二、认识Class类

Class类

java.lang.Class类:

image-20210217222236504

类的加载过程:程序经过javac.exe命令以后,会生成一个或多个字节码文件(.class结尾),接着我们使用java.exe命令对某个字节码文件进行解释运行。相当于将某个字节码文件加载到内容中(类的加载)。加载到内容中的类,我们称为运行时类,即为Class实例,一个类只有一个class对象。

注意:获得Class实例不能使用new来获取,则需要使用相应的方法获取。



获取Class实例的四种方式

加载到内存中的运行时类,会缓存一定的时间,在这段时间中,我们可以通过不同的方式来获取此运行时类。

四种方式如下

class Person {
}

public class Main {
    public static void main(String[] args) throws ClassNotFoundException {
        //方式一:调用运行时类的属性:类名.class
        Class<Person> class1 = Person.class;
        System.out.println(class1);

        //方式二:通过运行时类的对象调用getClass()获取
        Class<? extends Person> class2 = new Person().getClass();
        System.out.println(class2);

        //方式三:通过调用Class的静态方法:Class.forName(String classpath)
        Class<?> class3 = Class.forName("xyz.changlu.reflection.Person");
        System.out.println(class3);

        //方式四:通过使用类的加载器代用loadClass()方法获取
        ClassLoader loader = Main.class.getClassLoader();
        Class<?> class4 = loader.loadClass("xyz.changlu.reflection.Person");
        System.out.println(class4);

        //比较这四个实例是否相同
        System.out.println(class1 == class2);
        System.out.println(class1 == class3);
        System.out.println(class1 == class4);

    }
}
  • 这四种方式获取到的Class实例都是指向同一个实例内存空间,也就是说都是同一份。

image-20210217223656627



哪些类型可以是class对象?

class:如外部类、成员(成员内部类、静态内部类)、局部内部类、匿名内部类

interface:接口

[]:数组

enum:枚举类

annotation:注解@interface

primitive type:基本数据类型,如int、String

void

public class Main {
    public static void main(String[] args) throws ClassNotFoundException {
        Class<Object> class1 = Object.class;//类
        Class<Comparable> class2 = Comparable.class;//接口
        Class<String[]> class3 = String[].class;//一维数组(引用类型)
        Class<int[][]> class4 = int[][].class;//二维数组(基本数据类型)
        Class<ElementType> class5 = ElementType.class;//枚举类
        Class<Override> class6 = Override.class;//注解
        Class<Integer> class7 = int.class;//基本数据类型
        Class<Void> class8 = void.class;//void返回类型,也是一个类

        //元素类型与维度(指一维、二维)一样,就是同一个Class
        Class<? extends int[]> c9 = new int[10].getClass();
        Class<? extends int[]> c10 = new int[100].getClass();
        System.out.println(c9 == c10);//true
    }
}
  • 对于数组,只要其元素类型与维度相同那么它们的Class类实例都是同一个

三、反射的方法使用

获取构造器、类属性及方法

下面演示了构造器、类属性、类方法获取:

package xyz.changlu.reflection;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;

class Person {
    private String name;
    private Integer age;

    public Person() {
    }

    public Person(String name, Integer age) {
        this.name = name;
        this.age = 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 void say(){
        System.out.println("Person说话啦");
    }

    @Override
    public String toString() {
        return "xyz.changlu.reflection.Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}


public class Main {
    public static void main(String[] args) throws IllegalAccessException, InstantiationException, InvocationTargetException, NoSuchMethodException, NoSuchFieldException {
        Class clazz = null;
        try {
            clazz = Class.forName("xyz.changlu.reflection.Person");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        //1、构造器
        //    无参构造
        Person person = (Person) clazz.newInstance();
        System.out.println("反射创建无参构造:"+person);
        //    有参构造
        Constructor constructor = clazz.getConstructor(String.class, Integer.class);
        Person person1 = (Person) constructor.newInstance("changlu", 18);
        System.out.println("反射创建无参构造:"+person1);
        //    获取所有的构造器
        Constructor[] constructors = clazz.getConstructors();
        System.out.println("反射获取所有的构造器:"+Arrays.toString(constructors));
        //2、属性
        //   获取所有的公共属性(也包含父类所有的public属性)
        Field[] fields = clazz.getFields();
        System.out.println("反射获取所有公共属性:"+Arrays.toString(fields));
        //   获取当前运行类中所有的属性(包含private,单不包含继承的)
        Field[] declaredFields = clazz.getDeclaredFields();
        System.out.println("反射获取所有属性:"+Arrays.toString(declaredFields));
        Field name = clazz.getDeclaredField("name");
        name.setAccessible(true);//禁用访问安全检查的开关
        name.set(person,"Liner");
        System.out.println("反射修改person对象的name属性值:"+person.getName());
        //3、方法
        //    获取所有的公共方法
        Method[] method = clazz.getMethods();
        System.out.println("反射获取所有公共方法:"+Arrays.toString(method));
        //    获取所有的方法(不包含继承的)
        Method[] declaredMethods = clazz.getDeclaredMethods();
        System.out.println("反射获取所有方法:"+Arrays.toString(declaredMethods));
        //    执行指定方法say()
        Method method1 = clazz.getMethod("say");
        method1.invoke(person);
    }
}

image-20210217215225770

说明:Class的许多方法调用方法几乎都相同,无非就是获取指定的名称属性或是获取所有的属性(返回数组)

  • getFieldsgetDeclaredFields区别:第一个是获取对应类中的公共属性包含父类的,第二个是获取对应类中的所有属性不包含父类的。其他方法都相同。
  • 第77行的setAccessible(true),例如Mehtod、Field类都有该方法,值为true则指示反射的对象在使用时应该取消Java语言访问检查,默认是开启也就是false
    • 为什么要设置为true呢?因为JDK的安全检查耗时较多,所以对于属性设置、方法调用时通常会设置为true来达到提升反射速度的目的。


如何看待反射与封装性两个技术?

问题1:通过直接new的方式或反射方式都可以调用公共的结构,开发中如何使用?

  • 一般来说是直接使用new的方式(效率更高)。相对于反射的特征是其动态性能够动态的加载未知的外部配置对象,临时生成字节码进行加载使用,极大提高应用的扩展性。反射应用场景有Tomcat服务器、Spring的AOPIOC

问题2:反射机制与面向对象中的封装是不是矛盾,如何看待两个技术?

  • 封装性:通过设置权限符如private、public告知我们该私有的时候外部无法调用,公共的外部则允许调用,一些私有的方法内部会自行使用,体现了封装性。
  • 反射:告诉我们可以公共私有都可以调,但是不建议调私有的方法。
  • 简单点讲封装性解决的是建议你要不要调的问题,而反射则是能不能调的问题。


四、类的加载与ClassLoader理解

类的加载过程(含例子)

当程序主动使用某个类时,若类还未被加载到内存中,则系统会通过三个步骤对类进行初始化:类的加载(Load) => 类的链接(Link) => 类的初始化(Initialize)

  • 加载:将类的class文件字节码内容加载到内存,将这些静态数据转换成方法区的运行时数据结构,然后生成一个代表这个类的java.lang.Class对象,作为方法区中类数据的访问入口(即引用地址)。所有需要访问和使用类数据只能通过这个Class对象,该过程由类的加载器完成。
  • 链接:将类的二进制数据合并到JVM的运行状态之中。
    • 验证:确保加载的类信息符合JVM规范,保证加载类的正确性,包含四种验证。例如:以cafe开头,没有安全方面问题。
    • 准备:正式为类变量(static)分配内存并设置类变量默认初始值即零值,这些内存都将在方法区中进行分配。
    • 解析:虚拟机常量池内的符号引用(常量名)替换为直接引用(地址)的过程。
  • 初始化:JVM负责对类进行初始化。
    • 执行类构造器<clinit>()方法的过程。类构造器<clinit>()方法是由编译期自动收集类中所有类变量的赋值动作和静态代码块中的语句合并产生的。(类构造器是构造类信息的,不是构造该类对象的构造器)
    • 当初始化一个类时,若发现其父类还没有进行初始化,则需要先触发其分类的初始化。
    • 虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确加锁和同步。

大致过程是这样的(重要):对于一个类的初始化是包含上面三个步骤的,分为加载、链接、初始化。我们在理清一下思路,首先使用javac工具将.java源代码转为class字节码,接着使用java命令开始执行程序,jvm会先进行加载(使用类加载器)及链接的操作,而对于初始化这个步骤分为主动使用与被动使用。

  • JVM虚拟机规范规定了,每个类或者接口被java程序首次主动使用时才会对其进行初始化,不排除JVM在运行期间提前预判并初始化某个类。
  • 主动引用类方式:即初始化类
    • 启动类(重要):也就是执行main函数所在的类会导致该类的初始化
    • 使用new导致类的初始化(也会导致父类的初始化)
    • 访问类的静态变量(若是静态变量是其父类独有的,那么只会初始化父类;除了final常量)、静态方法
    • 对某个类进行反射操作(如Class.forName())
  • 被动引用类:不会发生类的初始化
    • 当通过子类引用父类的静态变量,不会导致子类初始化。
    • 通过数组定义类引用,不会触发初始化类。
    • 引用常量不会触发此类的初始化(常量在链接阶段就存入调用类的常 量池中了)。

见例子说明加载过程(引用一个博文例子)

情况一:②在①上面时

public class Main {
    private static Main instance = new Main(); // ②
    //  ①
    private static int x = 0;
    private static int y;

    private Main(){
        x++;
        y++;
    }

    public static Main getInstance(){
        return instance;
    }
    public static void main(String[] args) {
        Main singleton = Main.getInstance();
        System.out.println(singleton.x);
        System.out.println(singleton.y);
    }
}

image-20210218232651687

//详细过程
①加载:执行程序,将class文件加载到内存中,之后生成Class类,这个过程由类加载器完成
②链接:Main中的静态变量初始值赋值放置到方法区 instance=null  x=0  y=0
③初始化:
<clint>
    new Main() => 相当于执行x++,y++   此时x=1 y=1 instance=引用地址
    x=0  => 此时x=0  y=1
<clint>
//对于单独的int y;是没有赋值操作的所以还是1,最后输出结果则为0 1

情况二:①在②上面时

public class Main {
    //  ①
    private static int x = 0;
    private static int y;
    private static Main instance = new Main(); // ②

    private Main(){
        x++;
        y++;
    }

    public static Main getInstance(){
        return instance;
    }
    public static void main(String[] args) {
        Main singleton = Main.getInstance();
        System.out.println(singleton.x);
        System.out.println(singleton.y);
    }
}

image-20210218233637875

//详细过程
①加载:执行程序,将class文件加载到内存中,之后生成Class类,这个过程由类加载器完成
②链接:Main中的静态变量初始值赋值放置到方法区 x=0  y=0 instance=null
③初始化:
<clint>
    x=0  => 此时x=0  y=0
    new Main() => 相当于执行x++,y++  此时x=1 y=1 instance=引用地址
<clint>
//此时的new Main()是后执行的,所以会先执行x=0,之后执行构造器中内容,输出结果则为1 1

上面仅仅是我根据一些博客大致推出来的过程(如有问题,请求指出)



ClassLoader理解(各个类加载器)

认识各个类加载器ClassLoader

image-20210219182505602

ClassLoader(类加载器)作用:用来把类class装载进内存。

  • 类加载的作用:将class文件字节码内容加载到内存中,并将这些静态数据转换成方法区的运行时数据结构,然后在堆中生成一个代表该类的java.lang.Class对象,作为方法区中类数据的访问入口。
  • 类缓存:标准的JavaSE类加载器可以按要求查找类,但一旦某个类被加载到类加载器中,它将维持加载(缓存)一段时间。不过JVM垃圾回收机制可以回收这些Class对象。

JVM规范定义了如下类型的类加载器:

image-20210219182108376

  • 引导类加载器:用c++编写的,嵌在JVM内核中的加载器,主要负载加载JDK中$JAVA_HOME/jre/lib下的类库(包含核心类库),该加载器无法直接获取。(不是ClassLoader子类)
  • 扩展类加载器:用Java编写的,其父类加载器是Bootstrap,负责加载JRE的扩展目录,lib/ext或者由java.ext.dirs系统属性指定的目录中的JAR包的类。
  • 系统类加载器:也称为应用程序类加载器,父加载器是Extension,负责在JVM启动时加载来自Java命令的-classpath选项、java.class.path系统属性或者CLASSPATH换将变量所指定的JAR包和类路径,是最常用的类加载器。
    • 获取系统类加载器的方法:ClassLoader.getSystemClassLoader()

查看引导类加载器所加载的核心类库

public class Main {
    public static void main(String[] args) {
        URL[] urLs = Launcher.getBootstrapClassPath().getURLs();
        for (URL urL : urLs) {
            System.out.println(urL.toExternalForm());
        }
    }
}

image-20210219193624272

  • 我们可以看到其中也包含了rt.jar类库,这里我们就可以知道为什么使用核心类库时不需要导包了吧。

获取各个类加载器

public class Main {
    public static void main(String[] args) {
        //获取三个类加载器
        //①对于自定义类,其加载器是系统类加载器
        System.out.println(Main.class.getClassLoader());//sun.misc.Launcher$AppClassLoader@18b4aac2
        //②系统类加载器的父类加载器是扩展类加载器
        System.out.println(Main.class.getClassLoader().getParent());//sun.misc.Launcher$ExtClassLoader@677327b6
        //③扩展类加载器的父类是引导类加载器
        System.out.println(Main.class.getClassLoader().getParent().getParent());//null

        //测试JDK核心类的类加载器:引导类加载器
        System.out.println(String.class.getClassLoader());
    }
}
  • 第9行:获取ExtClassLoader的父类加载器结果为null,并不是说没有该加载器,是因为Bootstrap Classloader加载器是C++写的。


双亲委派机制

当一个类加载器收到一个类加载请求时,例如调用ClassLoader.loadClass("")方法,该方法会通过委派模型去加载类。

机制说明:当第一个类进行加载请求时,首先会在AppClassLoader中检查是否加载过该类,如果有则无需加载直接返回,如果没有那么会将这个请求委托给父类的加载器去执行,在父类加载器中同时也会检查是否加载过该类,若是没有则继续向上。直到Bootstrap Classloader之前都是会先去检查是否加载类。到BootStrap classloader会考虑自己是否能加载,若不能加载会依次让子加载器去依次去尝试加载,若是没有任何类加载器能够加载,最后会抛出ClassNotFoundException

首先看一下Launcher类:该类会在ClassLoader类中进行初始化

image-20210219204514996

  • 这里的BootClassPathHolder并不是指Bootstrap ClassLoader,该类可以使用其中方法获取到引导加载器类加载的类库。
  • 除了启动类加载器Bootstrap ClassLoader,其他的类加载器都是ClassLoader的子类。

我们看ClassLoader的源码

public abstract class ClassLoader {

    public Class<?> loadClass(String name) throws ClassNotFoundException {
        return loadClass(name, false);
    }
    
        protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            //检查是否已经加载过该类了
            Class<?> c = findLoadedClass(name);
            //若是没有加载过,则会调用父加载器
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    //若是父加载器不为空(说明此时有父类加载器)
                    if (parent != null) {
                        //会调用父类加载器的loadClass()方法,也就是本方法
                        c = parent.loadClass(name, false);
                    } else {
                        //父加载器为空,表示该类加载器的父类加载器就是Bootstrapclassloader了
                        //会直接使用引导类加载器Bootstrapclassloader去进行加载
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {//若是一直到最底层的子类加载器都加载不到
                }

                //父类加载器若是没有找到指定类
                if (c == null) {
                    long t1 = System.nanoTime();
                    //使用自己的findClass()方法去加载类
                    c = findClass(name);

                    // this is the defining class loader; record the stats
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }
}
  • 实现双亲委托机制(准确说是父委托机制)就是使用的该方法,其中进行了递归的操作。

为什么要使用双亲委派机制呢

  • Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关系能够避免类的重复加载。例如通过网络传输来的java.lang.String类进行类初始化时,在这种机制下这些系统级的类已经被BootStrapclassloader加载过了,所以其他类加载器就没有机会再去加载,从一定程度上防止了危险代码的植入。
  • 保证了系统级别的类的安全性,使一些基础类不会受到开发人员“定制化”的破坏。


加载properties文件

image-20210219211548069

import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;

public class Main {
    public static void main(String[] args) throws ClassNotFoundException, IOException {
        Properties properties = new Properties();
        //通过系统类加载器的getResourceAsStream()获取输入流
        InputStream is = ClassLoader.getSystemClassLoader().getResourceAsStream("jdbc.properties");
        properties.load(is);
        //获取键值对
        System.out.println(properties.getProperty("username") + ":" + properties.getProperty("password"));
    }
}
  • 注意这里的jdbc.properties需要放置到src目录下,否则会报空指针。

image-20210219211606994



五、获取运行时类的完整结构

通过反射技术调用Class的方法能够获取到类的完整结构:实现的全部接口继承的父类构造器方法属性注解权限修饰符返回类型参数列表参数异常

接口

  • public Class[] getInterfaces():确定此对象所表示的类或接口实现的接口

父类

  • public Class getSuperclass():表示此 Class 所表示的实体(类、接口、基本类型)的父类的 Class。

构造器

  • public Constructor[] getConstructors():返回此 Class 对象所表示的类的所有public构造方法(该对象必须显示声明才能获取到,无法获取父类的构造器)。
  • public Constructor[] getDeclaredConstructors():返回此 Class 对象表示的类声明的所有构造方法(默认的无参构造也能够获取到)。
  • Constructor类:包含的方法
    • public int getModifiers():取得修饰符。
    • public String getName();:取得方法名称。
    • public Class[] getParameterTypes();:取得参数的类型。

方法

  • public Method[] getMethods():返回此Class对象所表示的类或接口的public的方法(包括所有父类public方法)。
  • public Method[] getDeclaredMethods():返回此Class对象所表示的类或接口的全部方法(不包括父类的方法)。
  • Method类:包含的方法
    • public Class getReturnType():取得全部的返回值。
    • public Class[] getParameterTypes():取得全部的参数。
    • public int getModifiers():取得修饰符。
    • public Class[] getExceptionTypes():取得异常信息。

属性(Field):

  • public Field[] getFields():返回此Class对象所表示的类或接口的public的Field(包括所有父类public方法)。。
  • public Field[] getDeclaredFields():返回此Class对象所表示的类或接口的全部Field。
  • Field类:包含的方法
    • public int getModifiers():以整数形式返回此Field的修饰符。
    • public Class getType():得到Field的属性类型。
    • public String getName():返回Field的名称。

注解(Annotation):

  • Annotation[] getAnnotations():获取该类上的注解。

泛型

  • Type getGenericSuperclass():获取父类泛型类型。
  • getActualTypeArguments():获取运行时的带泛型的父类的泛型。

  • Package getPackage():获取当前类的包。

说明:这里只是举了一小部分方法,还有其他许多方法都类似,一定要熟悉反射包的作用,其反射机制。



六、调用运行时类的指定方法

1、调用指定方法(invoke方法)

前面介绍了获取方法类Method的方式,这里来学习通过反射调用类中的方法(Method类中的invoke方法):

首先我们需要从指定的Class中获取到指定的方法Method实例:

  • 调用方法Method getMethod(String name, Class<?>... parameterTypes)
    • name(参数):表示指定方法的名称
    • parameterTypes(参数):填写方法参数的类型,如String.class

接着使用指定Method实例来调用invoke方法

  • 调用方法:Object invoke(Object obj, Object... args)
    • obj(参数):该方法被调用的对象,若该方法是静态方法可填null。
    • args(参数):该方法填入的参数,若参数为空可填null或不填。
    • Object(返回值):对应原方法的返回值,若原方法无返回值,则返回null。

注意点:若是该方法是私有private,使用getDeclaredMethod方法获取实例,并在调用invoke方法之前使用method.setAccessible(true);才可调用私有方法。

测试一下

class Student{
    private void say(){
        System.out.println("新年快乐!");
    }
}

public class Main {
    public static void main(String[] args) throws Exception {
        Class<Student> clazz = Student.class;
        Method method = clazz.getDeclaredMethod("say");
        method.setAccessible(true);//不设置为true会报错java.lang.IllegalAccessException
        method.invoke(clazz.newInstance());//若是该方法是静态方法可以填null
    }
}

image-20210220001711994



2、调用指定属性

在反射机制中,可以直接通过Field类操作类中的属性,通过Field类提供的set()get()方法就可以完成设置和取得属性内容的操作。

①首先取得Filed类(指定属性),使用Class类的getField(String name)public Field getDeclaredField(String name)

  • name(参数):对应参数名称。

②通过调用Filed的方法取得与设置该属性的值。

  • public Object get(Object obj):取得指定对象obj上此Field的属性内容。
  • public void set(Object obj,Object value):设置指定对象obj上此Field的属性内容
    • 参数obj对于静态属性时可以填null直接获取与上面调用方法一致。

注意点:获取Class中指定私有属性需要使用getDeclaredField(String name)方法,并且在调用get()或set()前使用Field类的setAccessible(true);方法才能获取到。

测试一下

class Student{

    private String name;

}

public class Main {
    public static void main(String[] args) throws Exception {
        //获取属性name
        Class<Student> clazz = Student.class;
        Field field = clazz.getDeclaredField("name");
        field.setAccessible(true);
        System.out.println(field.get(clazz.newInstance()));

        //设置属性name为changlu
        Student student = clazz.newInstance();
        field.set(student,"changlu");//设置属性
        System.out.println(field.get(student));
    }
}

image-20210220002915238



setAccessible方法说明

MethodFieldConstructor类都有void setAccessible(boolean flag)方法。

  • flag可填true或false。
    • true:指示反射的对象在使用时应该取消Java语言访问检查。能够提升反射的效率,并使得原本无法访问的private属性或方法能够访问。
    • false:默认为此参数,指示反射的对象应该实施Java语言访问检查。

注意:无论publicprivate的属性、方法、构造器都可以使用该方法并设置true来取消java访问检查,能够提升反射效率(频繁反射操作建议关闭)。需要最最注意一点就是若是想要通过反射访问private的属性,一定要将访问检查取消,也就是要调用setAccessible(true),并且前面在获取FiledMethod类…需要使用如getDeclaredField()否则会报异常。



七、反射应用(动态代理)

1、介绍动态代理

代理设计模式原理:通过使用一个代理将对象包装起来,然后使用该代理对象取代原始对象。任何原始对象的调用都要通过代理。代理对象决定是否以及何时将方法调用转到原始对象上。

  • 代理分类:静态代理与动态代理。
    • 静态代理:代理类与目标对象的类都是在编译期间确定下来,不利于程序的扩展。同时每一个代理类只能为一个接口服务,这样一来程序开发中必然产生过多的代理。(最好就通过一个代理类完成全部的代理功能)
    • 动态代理:指客户通过代理类来调用其他对象的方法,并且是在程序运行时根据需要动态创建目标类的代理对象。使用场景如调试、远程方法调用。

动态代理相对于静态代理优点:抽象角色中(接口)声明的所有方法都转移到调用处理器一个集中的方法中处理,这样我们能够更加灵活和统一的处理众多的方法。



2、基于接口的动态代理

介绍Proxy、InvocationHandler

Java中动态代理的实现,关键两个类为ProxyInvocationHandler

  • Proxy:专门完成代理的操作类,是所有动态代理类的父类。通过此类为一个或多个接口动态地生成实现类。提供用于创建动态代理类和动态代理对象的静态方法:
    • static Class<?> getProxyClass(ClassLoader loader,Class<?>... interfaces):创建一个动态代理类所对应的Class对象。
    • static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h):直接创建一个动态代理类对象。
      • loader:传入类加载器。
      • interfaces:传入需要代理类实现的全部接口,数组形式。
      • h:实现InvocationHandler接口的实现类实例。
  • InvocationHandler接口:需要实现该接口并实现其中的invoke()方法。
    • Object invoke(Object proxy, Method method, Object[] args):完成代理的具体操作。
      • proxy:代理类的对象。
      • method:此时调用的方法。
      • args:调用方法时的参数。

注意:在运行中创建的动态代理对象的父类就是Proxy类。


JDK实现动态代理(两个例子)

第一个动态代理程序

首先定义了一个接口Person,接着使用JDK提供的Proxy类的newProxyInstance方法来创建实现Person的接口代理类对象,这种没有实现接口类但是在运行期间创建了一个接口对象的方式,称为动态代码。这里JDK提供的动态创建接口对象的参数,叫做动态代理

mport java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

/**
 * @ClassName ProxyTest
 * @Author ChangLu
 * @Date 2021/2/20 15:28
 * @Description TODO
 */

//被代理类接口
interface Person{
    void say(String name);

    String walk();
}

//实现InvocationHandler接口,并重写方法
class OwnInvocationHandler implements InvocationHandler {

    //注意这里的proxy是真实对象的代理类(这里指的是Person接口的代理类,父类是Proxy类)
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        //通过method实例的getName()来判断当前调用的是哪个方法
        if("say".equals(method.getName())){
            System.out.println("我是"+args[0]);
        }else if("walk".equals(method.getName())){
            System.out.println("在走路中.....");
        }
        return null;
    }
}

public class AnnotationTest {

    public static void main(String[] args) {
        //动态创建Person的代理类
        Person person = (Person) Proxy.newProxyInstance(Person.class.getClassLoader(), new Class[]{Person.class}, new OwnInvocationHandler());
        person.say("changlu");//我是changlu
        person.walk();//在走路中.....
    }
}
  • 第39行:这里的person实例实际上是Person接口的代理类,是在运行期间创建的一个接口对象。
    • 对于实现哪个接口的代理类根据Proxy.newProxyInstance传入的第二个参数相关,传入指定接口的Class类用数组形式传递。
  • 第35、36行:使用多态方式调用其接口方法时会执行OwnInvocationHandler(也就是实现InvocationHandler接口的实现类)的invoke方法。

image-20210224183305304


针对于实现类进行动态代理

若我们想要一个实现接口类进行方法增强,可以使用通过动态代理的方式实现,在实现接口类指定方法的前后添加代码功能:

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

/**
 * @ClassName ProxyTest
 * @Author ChangLu
 * @Date 2021/2/20 15:28
 * @Description TODO
 */

//被代理类接口
interface Person{
    void say(String name);

    String walk();
}

//创建Person的实现类
class Student implements Person{

    @Override
    public void say(String name) {
        System.out.println("student名字叫"+name);
    }

    @Override
    public String walk() {
        System.out.println("student跑步....");
        return "1000公里";
    }
}

class OwnInvocationHandler implements InvocationHandler{

    private Object obj;

    public OwnInvocationHandler(Object obj) {
        this.obj = obj;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println(method.getName()+"方法执行前...");
        //执行代理类调用的方法
        Object invokeReturn = method.invoke(obj, args);
        System.out.println(method.getName()+"方法执行后...");
        return invokeReturn;//将调用方法的返回值返回
    }
}

public class ProxyTest {

    public static void main(String[] args) {
        //传入指定的实体类
        Student student = new Student();
        InvocationHandler handler = new OwnInvocationHandler(student);
        //动态创建Person的代理类
        Person person = (Person) Proxy.newProxyInstance(Student.class.getClassLoader(), student.getClass().getInterfaces(),handler);
        //代理类开始执行方法了
        person.say("changlu");
        System.out.println("-----------------");
        person.walk();
    }
}

重要重要

  • 第38行:要想使用指定接口实现类的方法,在这里很关键的是在实现InvocationHandler中多增加了一个有参构造,用于将对应接口实现类实例传入进来,便于之后在invoke()(InvocationHandler接口实现类中的)中使用method.invoke(obj,参数)来反射调用接口实现类的指定方法。
  • 第46行:该行实际上就是通过反射来调用obj实例的方法,在此前后我们可以添加其他的相关操作。
  • 第59行:newProxyInstance()方法中的传参说明,第一个参数就是传入一个类加载器(并不限制于如上的获取类加载器方法),第二个参数比较重要,也就是你想要在运行期间实现的接口。(这里student.getClass().getInterfaces()也可以使用new Class[]{Person.class},获取到对应接口class类并且以数组形式传入)

说明:这个例子也可以说是SpringAOP的简单实现了,可用于日志记录等其他使用场景。



关于InvocationHandler接口中第一个参数proxy

InvocationHandler接口如下:

public interface InvocationHandler {
    public Object invoke(Object proxy, Method method, Object[] args)
        throws Throwable;
}

这里的proxy指的是在运行期间动态创建的接口对象,即真实对象的真实代理对象,代理对象为com.sun.proxy.$Proxy0

使用用途:通常情况下invoke方法都会返回真实对象的返回结果(如前面例2中调用实际类的方法返回值),该方法返回值为Object,我们也可以将proxy对象返回,可以通过这个返回对象做相关操作。

例子:返回proxy的用途如下

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

/**
 * @ClassName ProxyTest
 * @Author ChangLu
 * @Date 2021/2/20 15:28
 * @Description TODO
 */

interface Account {
    Account deposit(double value);

    double getBalance();
}

class OwnInvocationHandler implements InvocationHandler {

    private double balance;

    @Override
    public Object invoke (Object proxy, Method method, Object[] args) throws Throwable {

        if ("deposit".equals(method.getName())) {
            Double value = (Double) args[0];
            System.out.println("deposit: " + value);
            balance += value;
            return proxy; 
        }
        if ("getBalance".equals(method.getName())) {
            return balance;
        }
        return null;
    }
}
public class ProxyTest {

    public static void main(String[] args) {
        Account account = (Account) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[]{Account.class}, new OwnInvocationHandler());
        account.deposit(100).deposit(200).deposit(300);
        System.out.println(account.getBalance());
        account.deposit(100).deposit(200);
        System.out.println(account.getBalance());
    }
}
  • 第23行:在invoke()方法中通过反射调用无返回值的方法,返回proxy实例即真实代理类。我们可以看到41行可以对该代理对象进行连续调用。

这里又会有一个问题就是为什么不返回this,而是返回proxy

  • 答:若是在invoke()方法中返回this,就是返回的是OwnInvocationHandler实例了,而不是真实的代理对象。


3、JDK动态代理原理分析

我们通过上面InvocationHandler第一个参数proxy案例来进行过程原理分析:

首先看该方法:Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[]{Account.class}, new OwnInvocationHandler());

  • 使用的是Proxy类中的newProxyInstance方法。
//Proxy类
public class Proxy implements java.io.Serializable {
    @CallerSensitive
    public static Object newProxyInstance(ClassLoader loader,
                                          Class<?>[] interfaces,
                                          InvocationHandler h)
        throws IllegalArgumentException
    {
        Objects.requireNonNull(h);

        final Class<?>[] intfs = interfaces.clone();
        final SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            checkProxyAccess(Reflection.getCallerClass(), loader, intfs);
        }

        //通过传入方法的类加载器以及接口class类来创建出代理类
        Class<?> cl = getProxyClass0(loader, intfs);
		 ...
    }
}   
  • 第18行:getProxyClass0方法返回的代理类是在运行期间中创建的类名为$Proxy0,暂存在JVM中,我们在JDK核心类库中是找不到该代理类的。(该代理类的名称后面的0是编号,有多个代理类会一次递增)

由于该代理类是动态生成的类文件,暂时缓存在jvm中,我们通过下面方法获取到class文件:

interface Account {
    Account deposit(double value);

    double getBalance();
}

public class ProxyTest {

    public static void main(String[] args) throws IOException {
        //第一个为类名Proxy0,第二个为要实现的接口(数组形式传递)
        byte[]classFile = ProxyGenerator.generateProxyClass("Proxy0",new Class[]{Account.class});
        File file =new File("Proxy0.class");
        FileOutputStream fos =new FileOutputStream(file);
        fos.write(classFile);
        fos.flush();
        fos.close();
    }
}

参考文章

[1]. setAccessible(true)用法及意义

[2]. JAVA反射机制以及常见应用场景

[3]. Java类加载机制:双亲委派机制,还是应该叫做“父委派模型”?

[4]. 通俗易懂的双亲委派机制

[5]. java 类的加载过程 写的比较详细建议看

[6]. 一道面试题搞懂JVM类加载机制(类被初始化的几种情况,类文件加载的过程)

[7]. jvm之java类加载机制和类加载器(ClassLoader)的详解

动态代理:

[8]. 廖雪峰-动态代理 例子较少

[9]. InvocationHandler中invoke方法中的第一个参数proxy的用途

[10]. InvocationHandler中invoke()方法的调用问题 包含源码分析,建议反复阅读

[11]. java动态代理实现与原理详细分析 动态代理大致过程很详细

[12]. 获得JDK动态代理生成类$Proxy0的内容

[13]. 什么是动态代理?两种常用的动态代理方式

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。