Java:反射(基本概念,静态代理,动态代理,反射机制)

65 阅读17分钟

1,反射基础

1.1,反射定义

反射是Java 中的一种特性,它提供了在运行时获取类的信息,包括类的属性、方法、构造器等,并且可以动态地创建、访问和修改对象的属性和方法。 反射可以扩展 Java 的动态性和灵活性,但是也可能降低代码的可读性和性能。 这种动态获取信息、动态调用对象的方法的功能就称为 Java 语言的反射机制。

反射的基本原理(类加载过程1): 在通常情况下,一定是先有类然后再 new 一个对象出来的,类的正常加载过程是这样的:

  • 首先 JVM 会将我们的代码编译成一个 .class 字节码文件,然后被类加载器(ClassLoader)加载进 JVM 的内存中,同时会创建这个类的 Class 对象存到堆中(注意这个不是 new 出来的对象,而是类的类型对象)。
  • JVM 在创建这个类对象前,会先检查其类是否加载,寻找类对应的 Class 对象,若加载好,则为其分配内存,然后再进行初始化 new 操作。
  • 那么在加载完一个类后,堆内存的方法区就产生了一个 Class 对象,并且包含了这个类的完整结构信息,我们可以通过这个 Class 对象看到类的结构,就好比一面镜子。所以我们形象的称之为:反射。

反射的基本原理(类加载过程2):在通常情况下,一定是先有类再有对象,我们把这个通常情况称为 “正”。那么反射中的这个 “反” 我们就可以理解为根据对象找到对象所属的类(对象的出处)。通过反射,也就是调用了 getClass() 方法后,我们就获得了这个类类对应的 Class 对象,看到了这个类的结构,输出了类对象所属的类的完整名称,即找到了对象的出处。当然,获取 Class 对象的方式除了调用 getClass() 外还有另外三种方法。反射的优点就是比较灵活,能够在运行时动态获取类的实例。

反射的缺点:

  • 性能瓶颈:反射相当于一系列解释操作,通知 JVM 要做的事情,性能比直接的 Java 代码要慢很多。
  • 安全问题:反射机制破坏了封装性,因为通过反射可以获取并调用类的私有方法和字段。

反射的应用: 反射在我们实际编程中其实并不会直接大量的使用,但是实际上有很多设计都与反射机制有关。

  • 动态代理机制。
  • 使用 JDBC 连接数据库。
  • Spring / Hibernate 框架(实际上是因为使用了动态代理,所以才和反射机制有关,这个地方可以酌情扩展)。

1.2,静态代理

静态代理:对于你想要增强的委托类,我们需要新建一个代理类,这两个类实现一个同样的接口,然后将委托类注入进代理类中,在代理类的方法中调用委托类中的对应方法。这样,我们就可以通过代理类屏蔽对目标对象的访问,并且可以在目标方法执行前后做一些自己想做的事情。

从 JVM 层面来说, 静态代理就是在编译时就将接口、委托类、代理类这些都变成了一个个实际的 .class 文件。静态代理的弊端很明显,一个委托类对应一个代理类,多个委托类就需要新建多个代理类,能不能将代理类做成一个通用的呢? 动态代理!

1.3,动态代理

动态代理它是代理模式(设计模式)的一种,所谓代理模式就是,使用代理对象来代替对真实对象的访问,这样就可以在不修改原目标对象的前提下,提供额外的功能操作,扩展目标对象的功能。代理模式有三大角色:

  • Real Subject: 真实类,也就是被代理类、委托类。用来真正完成业务服务功能。
  • Proxy: 代理类。将自身的请求用 Real Subject 对应的功能来实现,代理类对象并不真正的去实现其业务功能。
  • Subject: 定义 RealSubjectProxy 角色都应该实现的接口。

同样的,JDK 动态代理需要委托类实现一个接口,不过代理类就不需要也实现同样的接口了,但是,JDK 动态代理机制中添加了一个新的角色,那就是处理类。具体来说:

  • 我们需要新建一个处理类,然后将委托类注入处理类。
  • 另外,这个处理类需要实现 InvocationHandler 接口,并重写其 invoke 方法。在 invoke 方法中可以利用反射机制调用委托类的方法,并可以在其前后添加一些额外的处理逻辑。
  • 最后,我们定义一个创建代理对象的工厂类(代理类),通过 Proxy.newProxyInstance() 创建委托类对象的代理对象。

动态代就是通过字节码技术生成一个子类,并在子类中拦截父类方法的调用(这也就是为什么说 CGLIB 是基于继承的了),织入额外的业务逻辑。关键词就是拦截,CGLIB 引入一个新的角色方法拦截器,让其实现接口 MethodInterceptor,并重写 intercept 方法,这里的 intercept 用于拦截并增强委托类的方法(和 JDK 动态代理 InvocationHandler 中的 invoke 方法类似),最后,通过 Enhancer.create() 创建委托类对象的代理对象。

Java领域的常用代理技术:

  • JDK动态代理: JDK动态代理是Java官方提供的一种动态代理实现方式。它要求目标对象必须实现一个接口,代理对象在运行时动态地生成一个实现了相同接口的代理类,并在代理类中通过反射调用目标对象的方法。 在实现上,JDK 动态代理通过 Proxy 类和 InvocationHandler 接口来实现,只能代理实现了接口的类,因为它是基于接口的动态代理。JDK 动态代理有一个最致命的问题是它只能代理实现了某个接口的实现类,并且代理类也只能代理接口中实现的方法,要是实现类中有自己私有的方法,而接口中没有的话,该方法就不能进行代理调用。
  • CGLIB: 为了解决这个问题,可以用 CGLIB 动态代理机制,CGLIB(Code Generation Library)CGLIB是一种基于ASM字节码操作库实现的高性能动态代理技术。 它不要求目标对象实现接口,可以代理任何类型的类,包括final类。CGLIB 在运行时动态地生成一个继承了目标对象的子类,并在子类中增加切面逻辑来实现对目标对象的增强。在实现上,CGLIB 通过 MethodInterceptor 接口来实现。

【JDK动态代理的优点】

  • 简单易用,使用标准Java接口来实现代理。
  • 安全可靠,官方提供,不需要引入第三方库。
  • 性能高效,代理对象使用接口,直接调用目标对象的方法,效率相对较高。

【CGLIB动态代理的优点】

  • 支持对类的代理,可以代理没有实现接口的类。
  • 执行效率高,因为生成的代理类是目标类的子类,不需要反射调用目标对象的方法。

【动态代理的应用】

  • AOP的代理规则
  • RPC框架

1.4,Java中的反射

反射API用来生成JVM中的类、接口或则对象的信息:

  • Class 类: 反射的核心类,可以获取类的属性,方法等信息。
  • Field 类: Java.lang.reflec 包中的类,表示类的成员变量,可以用来获取和设置类之中的属性值。
  • Method 类: Java.lang.reflec 包中的类,表示类的方法,它可以用来获取类中的方法信息或者执行方法。
  • Constructor 类: Java.lang.reflec 包中的类,表示类的构造方法。

反射的使用步骤:

  • 获取想要操作的类的 Class 对象,他是反射的核心,通过 Class 对象我们可以任意调用类的方法。
  • 调用 Class 类中的方法,既就是反射的使用阶段。
  • 使用反射 API 来操作这些信息。

2,Class类的定义

2.1,Class基本概念

在正常的情况下,需要先有一个类的完整路径引入之后才可以按照固定的格式产生实例化对象,但是在Java中也允许通过一个实例化对象找到一个类的完整信息,那么这就是Class类的功能。

class X{

}
public class Main {
    public static void main(String[] args) {
        X x = new X();
        System.out.println(x.getClass().getName());
    }
}
================================
Test.X

从程序的运行结果来看,通过一个对象得到了对象所在的完整的“包类”名称。

getClass()方法是Object类中的,此方法的定义如下:

public final Class getClass()

所谓的反射就是可以通过对象反射求出类的名称。

未命名绘图-第 4 页.drawio.png

所有类的对象实际上都是Class类的实例。JavaObject类是一切类的父类,那么所有类的对象实际上也就是java.lang.Class类的实例,所以所有的对象都可以转变为java.lang.Class类型表示。

Class 本身表示一个类的本身,通过 Class 可以完整地得到一个类中的完整结构,包括此类中的方法定义、属性定义等。

f1045764f33481db9a11f8aacd17367e.png

2.2,获取Class对象的方法

  • 调用某个对象的 getClass()方法
Person p=new Person();
Class clazz=p.getClass();
  • 调用某个类的 class 属性来获取该类对应的 Class 对象
Class clazz=Person.class;
  • 使用 Class 类中的 forName()静态方法(最安全/性能最好)(最常用)
Class clazz=Class.forName("类的全路径");
class X {

}

public class Main {
    public static void main(String[] args) throws ClassNotFoundException {
        Class<?> c1 = Class.forName("Test.X");
        Class<?> c2 = new X().getClass();
        Class<?> c3 = X.class;
        System.out.println(c1.getName() + "、" + c2.getName() + "、" + c3.getName());
    }
}
====================================
Test.X、Test.X、Test.X

3,创建对象的两种方式

3.1,通过无参构造参数实例化对象

如果想要通过 Class 类本身实例化其他类的对象,则可以使用 newInstance() 方法,但是必须要保证被实例化的类中存在一个无参构造方法。各种高级应用中都提倡类中存在无参构造方法。

class Person {
    private String name;
    private int age;
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }
    public String toString() {
        return "Person{" +"name='" + name + '\'' +", age=" + age +'}';
    }
}

public class Main {
    public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
        Class person = Class.forName("Test.Person");
        Person p = (Person) person.newInstance();
        p.setAge(30);
        p.setName("燕双嘤");
        System.out.println(p.toString());
    }
}
===========================================
Person{name='燕双嘤', age=30}

3.2,调用有参构造实例化对象

对于没有无参构造方法,可以通过其他的方式进行实例化操作,只是在操作时需要明确地调用类中的构造方法,并将参数传递进去之后才可以进行实例化操作。操作步骤如下:

  • 通过Class类中的 getConstructors() 取得本类中的全部构造方法。
  • 向构造方法中传递一个对象数组进去,里面包含了构造方法中所需的各个参数。
  • 之后通过 Constructor 实例化对象。

ff93f06fe8fbfabedae24cf592cc7388.png

这种实例化方法极其复杂,建议采用无参构造方法。

 4,反射的应用——取得类的结构

反射机制所提供的功能远远不止此,还可以通过反射得到一个类的完整结构,那么就需要使用java.lang.reflect包中的以下几个类:三个类都是AccessibleObject类的子类。

  • Constructor:表示类中的构造方法。
  • Field:表示类中的属性。
  • Method:表示类中的方法。
interface China{
    public static final String NATIONAL = "CHINA";
    public static final String AUTHOR = "燕双嘤";
    public void sayChina();
    public String sayHello(String name,int age);
}
class Person implements China{
    private String name;
    private int age;

    public Person() {
    }

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public void sayChina() {
        System.out.println(AUTHOR+"——————"+NATIONAL);
    }
    public String sayHello(String name, int age) {
        return name+"_______"+age;
    }
}

4.1,取得所实现的全部接口

要取得一个类所实现的全部接口,则必须使用Class类中的 getInterface() 方法。getInterfaces() 方法返回一个 Class 类的对象数组,之后直接利用 Class 类中的 getName() 方法输出即可。

public Class[] getInstances()
public class Main {
    public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException, InvocationTargetException {
        Class c = Class.forName("Test.Person");
        Class<?>[] cInstance = c.getInterfaces();
        for (int i=0;i<cInstance.length;i++){
            System.out.println(cInstance[i].getName());
        }
    }
}
==================================
Test.China

因为接口是类的特殊形式,而且一个类可以实现多个接口,所以此时Class数组的形式将全部的接口对象返回,并利用循环的方式将内容依次输出。

4.2,取得父类

一个类可以实现多个接口,但是只能继承一个父类,所以如果要取得一个类的父类,可以直接使用Class类中的 getSuperclass() 方法。

public Class<? super T> getSuperclass()

getSuperclass() 方法返回的是Class实例,和之前得到接口一样,可以通过 getName() 方法获取。

public class Main {
    public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException, InvocationTargetException {
       Class c = Class.forName("Test.Person");
        Class<?> c2 = c.getSuperclass();
        System.out.println("父类的名称:"+c2.getName());
    }
}
========================================
父类的名称:java.lang.Object

4.3,取得全部构造方法

要取得一个类中的全部构造方法,则必须使用 Class 类中的 getConstructors() 方法。

public class Main {
    public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException, InvocationTargetException {
        Class c = Class.forName("Test.Person");
        Constructor<?> con[] = c.getConstructors();
        for (int i=0;i<con.length;i++){
            System.out.println("构造方法:"+con[i]);
        }
    }
}
============================
构造方法:public Test.Person()
构造方法:public Test.Person(java.lang.String,int)

4.4,取得全部方法

要取得一个类中的全部方法,可以使用 Class 类中的 getMethods() 方法,此方法返回一个 Method 类的对象数组。而如果要想进一步取得方法的具体信息,例如,方法的参数、抛出异常声明等,则就必须依靠 Method 类,此类中的常用方法。

未命名绘图-第 4 页.drawio.png

da6013b8094beb50ab21e09391f1ada2.png

public class Main {
    public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException, InvocationTargetException {
        Class<?> c1 = Class.forName("Test.Person");
        Method[] m = c1.getMethods();
        for (int i = 0; i < m.length; i++) {
            Class<?> r = m[i].getReturnType();      //返回值类型
            Class<?>[] p = m[i].getParameterTypes();    //方法参数类型
            int xx = m[i].getModifiers();   //方法修饰符
            System.out.print(Modifier.toString(xx) + " ");  //还原修饰符
            System.out.print(r.getName() + " ");  //得到方法名称
            System.out.print(m[i].getName());     //取得方法名称
            System.out.print("(");
            for (int x = 0; x < p.length; x++) {    //输出参数
                System.out.print(p[x].getName() + " " + "arg" + x);
                if (x < p.length - 1) {
                    System.out.print(",");
                }
            }
            Class<?> ex[] = m[i].getExceptionTypes();
            if (ex.length > 0) {
                System.out.print(")throws ");
            } else {
                System.out.print(")");
            }
            for (int j = 0; j < ex.length; j++) {//输出异常信息
                System.out.print(ex[j].getName());
                if (j<ex.length-1){
                    System.out.print(",");
                }
            }
            System.out.println();
        }
    }
}

4.5,取得全部属性

在反射操作中可以取得一个类中的全部属性,但是在取得属性时由以下两种不同的操作。

  • 得到实现的接口或父类中的公共属性: public Field[] getField() throws SecurityException
  • 得到本类中的全部属性: public Field[] getDeclaredFields() throws SecurityExcption

1.png

public class Main {
    public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException, InvocationTargetException {
        Class<?> c1 = Class.forName("Test.Person");
        {
            Field f[] = c1.getDeclaredFields();
            for (int i = 0; i < f.length; i++) {
                Class<?> r = f[i].getType();
                int mo = f[i].getModifiers();
                String priv = Modifier.toString(mo);
                System.out.print("本类属性:");
                System.out.print(priv + " ");
                System.out.print(r.getName() + " ");
                System.out.print(f[i].getName() + " ");
                System.out.print(" ;");
            }
        }
        System.out.println();
        System.out.println("-----------------------------------");
        {
            Field f[] = c1.getFields();
            for (int i = 0; i < f.length; i++) {
                Class<?> r = f[i].getType();
                int mo = f[i].getModifiers();
                String priv = Modifier.toString(mo);
                System.out.print("本类属性:");
                System.out.print(priv + " ");
                System.out.print(r.getName() + " ");
                System.out.print(f[i].getName() + " ");
                System.out.print(" ;");
            }
        }
    }
}

5,Java反射机制的深入应用

反射除了可以取得一个类的完整结构外,还可以调用类中的指定方法或指定属性,并且可以通过反射完成对数组的操作。

5.1,通过反射调用类中的方法

如果要使用反射调用类中的方法可以通过Method类完成,操作步骤如下:

  • 通过 Class 类的 getMethod(String name,Class...parameterTypes) 方法取得一个 Method 的对象,并设置此方法操作时所需要的参数类型。
  • 之后才可以使用 invoke 进行调用,并向方法中传递要设置的参数。
Class<?> c1 = Class.forName("Test.Person");
Method met = c1.getMethod("sayChina");
met.invoke(c1.newInstance());

上面程序中通过 Class 类的 getMethod() 方法根据一个类中的方法名称获得 Method 对象,并通过 invoke 调用指定的方法。但是在 invoke() 方法时必须传入一个类的实例化对象,因为在 sayChina() 方法上没有任何的参数,所以此处没有设置参数类型和参数内容。

调用传参方法:

public class Main {
    public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException, InvocationTargetException, NoSuchMethodException {
        Class<?> c1 = Class.forName("Service.Person");
        Method met = c1.getMethod("sayHello", String.class, int.class);
        String str = (String) met.invoke(c1.newInstance(), "燕双嘤", 30);
        System.out.println(str);
    }
}
=============================================
燕双嘤_______30

5.2,调用setter及getter方法

从面向对象部分开始一直强调:类中的属性必须封装,封装之后的属性要通过 setter 及 getter 方法设置和取得,那么在使用反射的调用方法操作中,最重要的是调用类中的 setter 及 getter 方法。

public class Main {
    public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException, InvocationTargetException, NoSuchMethodException {
        Class<?> c1 = Class.forName("Service.Person");
        Object obj = c1.newInstance();
        setter(obj, "name", "燕双嘤", String.class);
        setter(obj, "age", 30, int.class);
        System.out.print("姓名:");
        getter(obj,"name");
        System.out.print("年龄:");
        getter(obj,"age");
    }

    public static String initStr(String old) {
        String str = old.substring(0, 1).toUpperCase() + old.substring(1);
        return str;
    }

    public static void setter(Object obj, String att, Object value, Class<?> type) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        Method met = obj.getClass().getMethod("set" + initStr(att), type);
        met.invoke(obj, value);
    }

    public static void getter(Object obj, String att) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        Method met = obj.getClass().getMethod("get" + initStr(att));
        System.out.println(met.invoke(obj));
    }
}

5.3,通过反射操作属性

在反射操作中虽然可以使用 Method 调用类中的 setter 及 getter 方法设置和取得属性,但是这样操作毕竟很麻烦,所以在反射机制中也可以直接通过 Field 类操作类中的属性,通过 Field 类提供的 set() 和 get() 方法就可以完成设置和取得属性内容的操作。但是在操作前首先需要注意的是,在类中的所有属性已经都设置成私有的访问权限,所以在使用 set() 或 get() 方法时首先需要使用 Field 类中的 setAccessible(true) 方法将需要操作的属性设置成可以被外部访问。

public class Main {
    public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException, InvocationTargetException, NoSuchMethodException, NoSuchFieldException {
        Class<?> c1 = Class.forName("Service.Person");
        Object obj = c1.newInstance();
        Field nameField = c1.getDeclaredField("name");
        Field ageField = c1.getDeclaredField("age");
        nameField.setAccessible(true);
        nameField.set(obj, "燕双嘤");
        ageField.setAccessible(true);
        ageField.set(obj, 30);
        System.out.println("姓名:" + nameField.get(obj));
        System.out.println("年龄:" + ageField.get(obj));
    }
}

代码操作形式上观察,可以非常清楚地发现,明显比之前使用setter或getter方法操作属性的代码更加简单、方便。

5.4,通过反射操作数组

反射机制不仅只能用在类上,还可以应用在任意的引用数据类型的数据上,当然,这本身就包含了数组,即可以使用反射操作数组。可以通过 Class 类的以下方法取得一个数组的 Class 对象。

public Class<?> getComponentType()

在反射操作包 java.lang.reflect 中使用 Array 类表示一个数组,可以通过此类取得数组长度,取得数组内容的操作。

2.png

public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException, InvocationTargetException, NoSuchMethodException, NoSuchFieldException {
    int temp[] = {1, 2, 3};
    Class<?> c = temp.getClass().getComponentType();    //取得数组Class对象
    System.out.println("类型:" + c.getName());      //取得数组类型的名称
    System.out.println("长度:" + Array.getLength(temp));    //取得数组的长度
    System.out.println("第一个内容:" + Array.get(temp, 0));     //得到第一个内容
    Array.set(temp, 0, 6);
    System.out.println("第一个内容:" + Array.get(temp, 0));
}
========================================
类型:int
长度:3
第一个内容:1
第一个内容:6

上面程序中通过Array类取得了数组的相关信息,并通过Array类中的set()方法修改了数组的内容。

在应用中还可以通过Array类根据已有的数组类型来开辟新的数组对象。

public class Main {
    public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException, InvocationTargetException, NoSuchMethodException, NoSuchFieldException {
        int temp[] = {1, 2, 3};
        int newTemp[] = (int[]) arrayInc(temp, 5);
        print(newTemp);
        System.out.println("-------------------------------------");
        String t[] = {"燕双嘤", "杜马", "步鹰"};
        String nt[] = (String[]) arrayInc(t, 8);
        print(nt);
    }

    public static Object arrayInc(Object obj, int len) {
        Class<?> c = obj.getClass();    //通过数组得到Class对象
        Class<?> arr = c.getComponentType();       //得到数组的Class对象
        Object newO = Array.newInstance(arr, len);       //重新开辟新的数组大小
        int co = Array.getLength(obj);      //取得数组长度
        System.arraycopy(obj, 0, newO, 0, co);  //复制数组内容
        return newO;
    }

    public static void print(Object obj) {
        Class<?> c = obj.getClass();
        if (!c.isArray()) {
            return;
        }
        Class<?> arr = c.getComponentType();
        System.out.println(arr.getName() + " 数组的长度是:" + Array.getLength(obj));
        for (int i = 0; i < Array.getLength(obj); i++) {
            System.out.print(Array.get(obj, i) + "、");
        }

    }
}
==================================================
int 数组的长度是:5
12300、-------------------------------------
java.lang.String 数组的长度是:8
燕双嘤、杜马、步鹰、nullnullnullnullnull、
Process finished with exit code 0