Java笔记 - 反射机制

反射机制的概述

想象一个应用场景,如果一个应用程序已经可以独立运行了,但是我们想要在它的基础上增加一些额外功能,所以就要用到接口。软件先提前写好接口Inter,然后我们只要用一个类Demo去实现这个接口,再在软件中运行Inter in = new Demo();就可以了。但是如果软件不是我们写的,我们不能修改源代码在其中new对象,就没有办法了。

所以我们换一种思考方式,我们可以使用一个配置文件,然后应用程序在运行的时候去读取配置文件中的信息,在我们用类Demo实现Inter接口后,我们把类名Demo写进这个配置文件中。当程序用流读取配置文件后,就根据类名Demo去寻找对应的class文件。如果找到class文件就加载文件并获取文件中的内容,获取到之后就可以对文件中的方法、字段等进行调用。

如果想要对指定名称的字节码文件进行加载,获取其中的内容并调用,就用到了反射

举例说明:
如果我们前面所说的应用程序就是Tomcat,它提供了处理请求和应答的方式。因为具体的处理动作不同,所以对外提供了接口Servlet。我们可以实现Servlet接口,自己定义具体请求和应答的处理方式。我们实现Servlet接口的类就是MyServlet类。Tomcat提供了一个配置文件web.xml,我们只需要把我们的类名Myservlet写到这个配置文件中去就行了。Tomcat会自己去读取配置文件中的指定名称。然后根据指定名称找到class文件(最终反射的就是class文件),然后就把这个文件加载进程序的。其实这些动作不是我们做的,是Tomcat自己做的。

反射机制的原理

反射机制原理

一般的类是我们根据具体对象的共性内容向上抽取而来的,而我们写完源程序之后编译得到的字节码文件就是class文件,class文件中也包含类名,字段,方法名等内容。Java把这些字节码文件的共性内容也向上抽取得到了一个对class文件进行描述的类就是java.lang包下的Class类。

我们用这个类就能获取到字节码文件中的内容,也就是说如果想对一个类文件进行解剖,只要获取到该类的字节码文件对象即可。

JAVA反射机制是在运行状态中,对于任意一个类(class文件),都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性;这种动态获取信息以及动态调用对象方法的功能称为java语言的反射机制。动态获取类中的信息,就是java反射。

获取字节码文件对象

想要对字节码文件进行解剖,必须要有字节码文件对象,如何获取字节码文件对象呢?
获取字节码文件对象有三种方法:
1.Object类中的getClass()方法

public static void getClassObject_1() {
    //这种方法必须要明确具体的类并创建对象
    Person p1 = new Person();
    Class clazz1 = p1.getClass();

    Person p2 = new Person();
    Class clazz2 = p2.getClass();

    System.out.println(clazz1==clazz2);
}

输出结果:

true

2.通过静态属性.class

public static void getClassObject_2() {
    //需要用到类中的静态成员.class
    Class clazz1 = Person.class;
    Class clazz2 = Person.class;
    System.out.println(clazz1 == clazz2);   
}

输出结果:

true

3.通过Class类中的forName方法

public static void getClassObject_3() throws ClassNotFoundException {
    String name = "cn.hellobottle.day28.Person";
    Class clazz = Class.forName(name);
    System.out.println(clazz);
}

输出结果:

class cn.hellobottle.day28.Person

构造函数的反射

如果是以前我们要创建一个类的对象

cn.hellobottle.bean.Person p = new cn.hellobottle.bean.Person();

运行程序,就会在classpath路径内寻找名为cn.hellobottle.bean.Person的字节码文件并将其加载进内存,在堆内存中开辟空间。

但是现在我们只有一个类名,怎么创建类的实例对象?
通过空参数的构造函数创建对象

public static void creatNewObject_1(){
    //类名
    String name = "cn.hellobottle.bean.Person";
    //通过类名获取其字节码文件对象
    Class clazz = Class.forName(name);
    //创建此Class对象所表示的类的一个新实例对象
    Object obj = clazz.newInstance();
}

输出结果:

空参数构造函数run

查看API文档说明
newInstance():创建此 Class 对象所表示的类的一个新实例。如同用一个带有一个空参数列表的 new 表达式实例化该类。如果该类尚未初始化,则初始化这个类。
其实就是用该方法调用class文件中的空参数构造函数。
这种方法和上面直接new对象的方法都执行了空参数的构造函数。

通过非空参数构造函数创建对象
如果想调用非空参数构造函数创建对象,就是要通过指定的构造函数进行对象的初始化,所以应该先获取到该构造函数。既然我们之前已经拿到了该类的字节码文件对象,所以就可以通过对象调用Class类的方法来完成。

getConstructor(Class< ?>…paramterTypes):能拿到指定的公共构造函数
getDeclaredConstructor(Class< ?>… parameterTypes):能拿到指定的所有构造函数

public static void creatNewObject_2(){
    String name = "cn.hellobottle.bean.Person";
    Class clazz = Class.forName(name);
    Constructor con = clazz.getConstructor(String.class, int.class);
    Object obj = con.newInstance("张三",16);
}

字段的反射

public公有字段
如果想获取公有字段,就要使用方法
getField(String name):返回一个指定的公有Field对象
getFields():返回所有的公有Field对象的数组
在获取到Field对象后如果想对其进行操作,就需要使用Field类中的set和get方法。

Class clazz = Class.forName("cn.hellobottle.bean.Person");

Field f1 = clazz.getField("name");//只能获取公有的,但可以是从父类继承来的
Object obj = clazz.newInstance();

f1.set(obj,"张三");
Object o = f1.get(obj);
System.out.println(o)

输出结果:

张三

private私有字段
如果想要操作私有字段,会遇到两个问题,一个是如何获取私有字段,一个是如何set和get私有字段
获取就使用
getDeclaredField(String name):获取一个指定的公有或私有Field对象
getDeclaredFields():获取所有的公有或私有Field对象的数组

如果想要set和get私有字段,查看API文档,Field类有一个父类AccessibleObject,它是类Field、Method和Constructor的基类。它提供了将反射的对象标记为在使用时取消默认 Java 语言访问控制检查的能力。
也就是说它提供了方法可以对私有字段取消检查权限,暴力访问。这个方法就是setAccessible(boolean flag)

Class clazz = Class.forName("cn.hellobottle.bean.Person");

Field f2 = clazz.getDeclaredField("age");//可以获取全部的,但只能是本类
Object obj = clazz.newInstance();

f2.setAccessible(true);
f2.set(obj, 25);
Object o = f2.get(obj);

System.out.println(o);

输出结果:

25

一般函数的反射

我们通过字节码文件对象获取构造函数只要确定参数列表就可以了,因为都是重载形式。而获取一般函数的时候不但要确定参数列表还要确定方法名。
获取到指定函数之后,如果想要调用函数,就要用到Method类中的方法
invoke(Object obj, Object… args):obj - 从中调用底层方法的对象,args - 用于方法调用的参数
带参数的一般函数

Class clazz = Class.forName("cn.hellobottle.bean.Person");
Method method = clazz.getDeclaredMethod("paramMethod", String.class, int.class);
Object obj = clazz.newInstance();

method.invoke(obj, "张三", 23);

输出结果:

paramMethod run.....张三:23

私有方法

Class clazz = Class.forName("cn.hellobottle.bean.Person");
Method method = clazz.getDeclaredMethod("privateMethod", null);
Object obj = clazz.newInstance();
method.setAccessible(true);

method.invoke(obj, null);

输出结果:

 method run 

动态代理

创建动态代理

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

这里写图片描述

Proxy 提供用于创建动态代理类和代理对象的静态方法, 它也是所有动态代理类的父类.

Proxy 提供了两个方法来创建动态代理类和动态代理实例

ArithmeticCalculator.java

public interface ArithmeticCalculator {

    int add(int i, int j);
    int sub(int i, int j);

    void mul(int i, int j);
    void div(int i, int j);
}

ArithmeticCalculatorImpl.java

public class ArithmeticCalculatorImpl implements ArithmeticCalculator {

    @Override
    public int add(int i, int j) {
        int result = i + j;
        return result;
    }

    @Override
    public int sub(int i, int j) {
        int result = i - j;
        return result;
    }

    @Override
    public void mul(int i, int j) {
        int result = i * j;
        System.out.println(result);
    }

    @Override
    public void div(int i, int j) {
        int result = i / j;
        System.out.println(result);
    }
}

ProxyTest.java

public class ProxyTest {

    @Test
    public void testCalculator(){

        final ArithmeticCalculator arithmeticCalculator = new ArithmeticCalculatorImpl();
        /**
         * ClassLoader: 由动态代理产生的对象由哪个加载器来加载
         * 通常情况下和被代理对象使用一样的类加载器
         * Class<?>[]: 由动态代理产生的对象必须需要实现的接口的Class数组
         * InvocationHandler: 当具体调用代理对象的方法时,将产生什么行为
         */
        ArithmeticCalculator proxy = (ArithmeticCalculator) Proxy.newProxyInstance(arithmeticCalculator.getClass().getClassLoader(),
                new Class[]{ArithmeticCalculator.class}, 
                new InvocationHandler(){
                    /**
                     * proxy: 
                     * method: 正在被调用的方法
                     * args: 调用方法时传入的参数
                     */
                    @Override
                    public Object invoke(Object proxy, Method method,
                            Object[] args) throws Throwable {
                        System.out.println("~The method " + method.getName() + "begin with " + Arrays.asList(args));
                        //调用被代理类的目标方法
                        Object result = method.invoke(arithmeticCalculator, args);

                        System.out.println("~The method " + method.getName() + "end with " + result);
                        return result;
                    }
        });

        proxy.mul(1, 2);
        int result = proxy.add(2, 5);
        System.out.println(result);
    }
}

关于动态代理的细节:
1.需要一个被代理的对象
2.类加载器通常是和被代理对象使用相同的类加载器
3.一般的,Proxy.newInstance() 的返回值是一个被代理对象实现的接口的类型
当然也可以是其他的接口类型
注意:第二个删除,必须是一个接口类型的数组
提示:若代理对象不需要额外实现被代理对象实现的接口以外的接口,可以使用target.getClass().getInterface()
4.InvocationHandler 通常使用匿名内部类的方式:被代理对象需要是final 类型的
5.InvocationHandler 的invoke() 方法中的第一个参数Object类型的proxy指的是正在被返回的那个代理对象,一般情况下不使用它。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值