JavaSE—反射与动态代理

反射

1. Java反射机制概述

  • Reflection(反射)是被视为动态语言的关键,反射机制允许程序在执行期借助于Reflection API取得任何类的内部信息,并能直接操作任意对象的内部属性及方法。

  • 加载完类之后,在堆内存的方法区中就产生了一个Class类型的对象(一个类只有一个Class对象),这个对象就包含了完整的类的结构信息。我们可以通过这个对象看到类的结构。这个对象就像一面镜子,透过这个镜子看到类的结构,所以,我们形象的称之为:反射

Java的反射机制就是为了体现动态的特性。

框架的底层源码会大量的使用反射。

框架 = 反射 + 注解 + 设计模式

  • Java反射机制提供的功能

    • 在运行时判断任意一个对象所属的类
    • 在运行时构造任意一个类的对象
    • 在运行时判断任意一个类所具有的成员变量和方法
    • 在运行时获取泛型信息
    • 在运行时调用任意一个对象的成员变量和方法
    • 在运行时处理注解
    • 生成动态代理(主要应用于SpringAOP代理模式
  • 反射相关的主要API

    • java.lang.Class:代表一个类 反射的源头
    • java.lang.reflect.Method:代表类的方法
    • java.lang.reflect.Field:代表类的成员变量
    • java.lang.reflect.Constructor:代表类的构造器

假如有一个Person类,在这个Person类外部,不可以通过Person类的对象调用其内部私有(private)结构

但是通过反射,我们可以做到调用Person类的私有结构;比如:私有的构造器、方法、属性。

疑问1:通过直接new的方式或反射的方式都可以调用公共的结构,开发中到底用哪个?

建议:直接new的方式

简单举个例子:

比如现在有前端代码和后端代码,后端代码已经部署在服务器并处于运行状态,现在通过浏览器访问后台,访问有可能是登录功能,也有可能是注册功能等等,假如当前的一个url是localhost/login,后端解析完发现是登录请求,这个时候我获取到的值是login,此时我的后端就new一个login的类对象,如果传进来的url是localhost/regist,那么就造一个regist的类对象,此时的操作都是动态的。

这个情景的实现特性就需要反射来做。

image-20220417234111906

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

不矛盾。理论上看起来是矛盾的,封装性告诉我们出去之后私有的不要用,但反射还可以用,这不是白封装了吗?

对于一个类的封装性,它其实在告诉我们,私有的方法就不要用了,因为私有方法是人家内部使用的方法,有可能这个私有方法已经被公有方法调用;

封装性的体现就是:如果你需要调用 只调用公有方法即可,私有方法你用不到

对于单例模式同样,有一个私有的构造器,其实在提示我们,你如果想new对象就不要用这个私有的,我已经把对象造好了,你直接用就行。

那么对于反射,有可能这个类提供的公有方法更好,比私有的完善很多,其实是建议你用公有方法的,如果你非要用反射调用私有方法,也允许,但是违背了这个类公有私有定义的意义。

对于单例模式,我已经把这个对象提供给你了,既然是单例就意味着没有必要造其它的对象,直接拿这个对象就可以完成项目的需求,如果你非要用反射调用私有的构造器,然后再造一个对象,你做的事其实跟它自己做的没有区别,所以是一个多余的操作。

封装性解决的是 建议你去调什么,而反射解决的是 我能不能调的问题。


2. 理解Class类并获取Class实例

  • 关于java.lang.Class的理解
  1. 类的加载过程

程序经过javac.exe命令后,会生成一个或多个字节码文件(.class结尾)

接着我们使用java.exe命令对某个字节码文件进行解释运行,相当于某个字节码文件加载到内存中,此过程就成为类的加载。

加载到内存中的类,我们就称为运行时类,次运行时类,就作为Class的一个实例。

  1. 换句话说,Class的实例就对应着一个运行时类。
  2. 加载到内存中的运行时类,会缓存一定的时间;在此时间之内,我们可以通过不同的方式来获取次运行时类,如下:
  • 获取Class类的实例(四种方法)
  1. 通过类的class属性获取
Class clazz1 = Person.class;
// 前提:已知具体的类
// 该方法最为安全可靠,程序性能最高 但体现不出反射的动态性
  1. 调用该实例的getClass()方法(Object类中的方法)获取Class对象
Person person = new Person();
Class clazz2 = person.getClass();
// 前提:已知某个类的实例
// 经常用于 判断这个对象所属的类
  1. 通过Class类的静态方法forName()获取,可能抛出ClassNotFoundException
Class clazz3 = Class.forName("全类名"); // 全类名例如:com.lyc.Person、java.lang.String
// 前提:已知一个类的全类名
// 常用 能更好的体现反射的动态性
  1. 通过类的加载器
ClassLoader classLoader = Person.class.getClassLoader();
Class class4 = classLoader.loadClass("全类名");
// 注意 我们自定义的类使用的基本都是 System Class Loader 系统类加载器
// 所以可以借用我们自定义的其他类的类加载器去加载类 生成一个Class对象
// 不常用 了解即可

3. 类的加载与ClassLoader的理解

  • 类的加载过程

image-20220418133817743

  • 类加载器的作用

image-20220419170408254

  • 类加载器的分类

image-20220418145526057

4. 创建运行时类的对象

public class Main {
    public static void main(String[] args) throws IllegalAccessException, InstantiationException {
        Class<Person> clazz = Person.class;
        /*
            newInstance():调用此方法,创建对应的运行时类的对象,内部调用了运行时类的空参构造方法

            要想此方法正常的创建运行时类的对象,要求:
            1.运行时类必须提供空参的构造器
            2.空参的构造器的访问权限要够 通常 设置为public

            在javabean中要求提供一个public的空参构造器 原因:
            1.便于通过反射,创建运行时类的对象
            2.便于子类继承此运行时类,默认调用super()时,保证父类有此构造器
         */
        Person p = clazz.newInstance();
        System.out.println(p);
    }
}

5. 获取运行时类的完整结构

  • Class类的方法
    • getName() 获得全类名
    • getSimpleName() 获得类名
    • getPackage() 返回类的包对象
    • getFields 返回本类和父类中声明为public的属性
    • getMethods() 返回本类和父类中声明为public的方法
    • getConstructors() 返回本类声明为public的构造器
    • newInstance() 创建指定类的对象 调用的是类的无参构造器
    • getDeclaredField(String fieldname) 通过属性名 获得属性对象
    • getDeclaredFields() 只能获取本类中所有的属性,返回一个Field数组
    • getDeclaredMethod(String methodName, Class ... argType) 根据方法名、参数列表获得一个方法对象
    • getDeclaredMethods() 获得本类中所有的方法
    • getDeclaredConstructor(Class ... argType) 根据构造器的参数列表获得一个构造器对象
    • getDeclaredConstructors() 获得本类中的所有的构造器,返回Constructor数组
    • getClassLoader() 获取当前类的类加载器
    • getInterfaces() 获取当前类实现的接口(动态代理会用到)
    • isXXX() 判断当前的Class对象的类型是否是某个类型
    • toString() 输出当前Class对象的类型信息 + 全类名
    • isAnnotationPresent(Class annotationClass) 判断某个类对象是否含有指定的注解

小 结:

  1. 在实际的操作中,取得类的信息的操作代码,并不会经常开发。

  2. 一定要熟悉java.lang.reflect包的作用,反射机制。

  3. 如何取得属性、方法、构造器的名称,修饰符等。

6. 调用运行时类的指定结构

  • 反射获取属性并使用
import java.lang.reflect.Field;

public class Main {
    public static void main(String[] args) throws Exception {
        Class clazz = Person.class;
        // 1.创建运行时类的对象
        Person p = (Person) clazz.newInstance();
        // 2.获取指定变量名的属性
        Field name = clazz.getDeclaredField("name");
        // 3.设置为true 保证当前属性是可访问的(否则只可以对public操作 private等无法进行get set)
        name.setAccessible(true);
        // set() 设置当前属性的值 参数1:指明设置哪个对象的属性 参数2:将此属性值设置为多少
        name.set(p, "xiao_cheng");
        // get() 获取当前属性的值 参数1:获取哪个对象的当前属性值
        String pName = (String) name.get(p);
        System.out.println(pName);
    }
}
  • 反射获取方法并使用
import java.lang.reflect.Method;

public class Main {
    public static void main(String[] args) throws Exception {
        Class clazz = Person.class;
        // 1.创建运行时类的对象
        Person p = (Person) clazz.newInstance();
        // 2.获取指定的某个方法 getDeclaredMethod() 参数1:指明获取方法的名称 参数2:指明获取的方法的形参列表
        Method show = clazz.getDeclaredMethod("show", String.class);
        // 3.保证当前方法是可访问的
        show.setAccessible(true);
        // 调用invoke()使用方法 参数1:方法的调用者 参数2:给方法形参赋值的实参 invoke()的返回值就是被调用的方法的返回值
        Object returnVal = show.invoke(p, "XIAO_CHENG");
        System.out.println(returnVal);
        
        // 获取并调用静态方法 private static void showDesc()
        Method showDesc = clazz.getDeclaredMethod("showDesc");
        showDesc.setAccessible(true);
        Object returnValue = showDesc.invoke(Person.class);
        System.out.println(returnValue); // 方法返回值为void 输出null
    }
}
  • 反射获取构造器并使用
import java.lang.reflect.Constructor;
// 不常用 最常用的还是上面的 Person p = clazz.newInstance();
public class Main {
    public static void main(String[] args) throws Exception {
        Class clazz = Person.class;
        // 1.获取指定的构造器 getDeclaredConstructor() 参数:指明构造器的参数列表
        Constructor constructor = clazz.getDeclaredConstructor(String.class);
        // 2.保证构造器可访问
        constructor.setAccessible(true);
        // 3.调用此构造器创建运行时类的对象 newInstance()方法创建对象 参数:指明构造器的参数列表
        Person p = (Person) constructor.newInstance("xiao_cheng");
        System.out.println(p);
    }
}

image-20220418223520242

7. 反射的应用:动态代理

对于静态代理,每一个被代理类都需要一个代理类,这样一来程序开发中必然产生过多的代理;

那我们能不能写一个通用的代理类呢?当然这个类不可能在编译期间确定下来,因为我实现了一个类的接口,另一个类就无法使用了。

所以我们需要在运行期间创建代理类,这时就需要反射来获取被代理类的接口,实现接口创建对应的代理类。

最好可以通过一个代理类完成全部的代理功能 —— 动态代理

  • 动态代理是指客户通过代理类来调用其它对象的方法,并且是在程序运行时根据需要动态创建目标类的代理对象。

  • 动态代理使用场合:

    • 调试

    • 远程方法调用

  • 动态代理相比于静态代理的优点:

    • 抽象角色中(接口)声明的所有方法都被转移到调用处理器一个集中的方法中处理,这样,我们可以更加灵活和统一的处理众多的方法。

我们通过反射就是想写这些通用的功能。

动态代理简单实现

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

/*
    公共接口 代理类与被代理类都需要实现
    method1: getBelief(): 获取信仰
    method2: eat(): 吃
 */
interface Human {

    String getBelief();

    void eat(String food);

}
/*
    被代理类
 */
class SuperMan implements Human {

    @Override
    public String getBelief() {
        System.out.println("I am a superman!");
        return "I am a superman!";
    }

    @Override
    public void eat(String food) {
        System.out.println("我喜欢吃" + food);
    }
}
/*
    要想实现动态代理,需要解决的问题
    问题一:如何根据加载到内存中的被代理类,动态的创建一个代理类及其对象。
    问题二:当通过代理类的对象调用方法a时,如何动态的去调用被代理类中的同名方法a。
 */
class ProxyFactory {
    // 调用此方法 返回一个代理类的对象。解决问题一
    public static Object getProxyInstance(Object obj) { // obj:被代理类的对象
        MyInvocationHandler handler = new MyInvocationHandler();
        handler.bind(obj); // 通过被代理对象赋值
        // Proxy:专门完成代理的操作类,是所有动态代理类的父类,通过此类为一个或多个接口动态地生成实现类。
        // newProxyInstance():创建一个代理类对象 参数1:被代理类的加载器 参数2:被代理类实现的接口 参数3:InvocationHandler接口
        return Proxy.newProxyInstance(obj.getClass().getClassLoader(), obj.getClass().getInterfaces(), handler);
    }
}

class MyInvocationHandler implements InvocationHandler {

    private Object obj; // 需要使用被代理类的对象进行赋值

    public void bind(Object obj) {
        this.obj = obj;
    }

    // 当我们通过代理类的对象,调用方法a时,就会自动的调用如下的方法:invoke()
    // 将被代理类要执行的方法a的功能就声明在invoke()中
    // invoke() 参数1:代理类对象 参数2:代理类对象调用的方法 参数3:同名方法的参数
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // method:即为代理类对象调用的方法 此方法也就作为了被代理类对象要调用的方法
        // obj:被代理类的对象
        Object returnValue = method.invoke(obj, args);
        // 上述方法的返回值就作为当前类中的invoke()的返回值
        return returnValue;
    }
}

public class ProxyTest {
    public static void main(String[] args) {
        SuperMan superMan = new SuperMan();
        Human proxyInstance = (Human) ProxyFactory.getProxyInstance(superMan);
        // 当通过代理类对象调用方法时,会自动的调用被代理类中同名的方法
        proxyInstance.getBelief();
        proxyInstance.eat("麻辣烫");
    }
}

代理类并没有在编译的时候显示,而是在运行时根据传进来的被代理类创建的,体现了反射的动态性。

  • 动态代理与AOP(Aspect Orient Programming) 面向切面编程

    • 使用Proxy生成一个动态代理时,往往并不会凭空产生一个动态代理,这样没有太大的意义。通常都是为指定的目标对象生成动态代理

    • 这种动态代理在AOP中被称为AOP代理,AOP代理可代替目标对象,AOP代理包含了目标对象的全部方法。但AOP代理中的方法与目标对象的方法存在差异:AOP代理里的方法可以在执行目标方法之前、之后插入一些通用处理

比如我们有三个代码段都有一部分相同的代码,此时我们可以把相同代码写成方法A,分别让这三个方法都调用方法A

image-20220419203908396

此时就是我们AOP的思想,面向切面编程,中间的方法是动态的,也就是不在三个代码块中把方法A写死:

image-20220419203816872

利用代码理解这个图片,对上方的代码稍加修改:

// 写一个类名为HumanUtil
class HumanUtil {
    public void method1() {
        System.out.println("================= 通用方法一 =================");
    }

    public void method2() {
        System.out.println("================= 通用方法二 =================");
    }
}
// 在invoke()方法添加两行HumanUtil类的通用方法
class MyInvocationHandler implements InvocationHandler {

    private Object obj; // 需要使用被代理类的对象进行赋值

    public void bind(Object obj) {
        this.obj = obj;
    }

    // 当我们通过代理类的对象,调用方法a时,就会自动的调用如下的方法:invoke()
    // 将被代理类要执行的方法a的功能就声明在invoke()中
    // invoke() 参数1:代理类对象 参数2:代理类对象调用的方法 参数3:同名方法的参数
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        HumanUtil util = new HumanUtil();
        util.method1(); // 通用方法一
        
        // method:即为代理类对象调用的方法 此方法也就作为了被代理类对象要调用的方法
        // obj:被代理类的对象
        Object returnValue = method.invoke(obj, args);

        util.method2(); // 通用方法二

        // 上述方法的返回值就作为当前类中的invoke()的返回值
        return returnValue;
    }
}

通过控制台可以看出,我们中间的方法是动态的,这就是AOP(面向切面编程)的体现

image-20220419211414105

文章整理自:尚硅谷,让天下没有难学的技术

  • 10
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小成同学_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值