• java反射和代理


      这一篇我们说说反射和动态代理,为什么这两个要一起说呢?因为动态代理中会用到反射,而且java中反射的用处太多了,基本上无处不在,而且功能十分强大;

    1.反射简介

      反射是什么呢?一般都是很专业的说法:在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;我最初看这句话我是没看出来什么厉害的地方,运行状态?什么是运行状态啊?

      简单看看下面这个图,类的加载机制以前说过了,这里就随意看看,一个类的字节码文件通过类加载器加载到jvm的堆中最终会生成一个该类的Class对象,注意,一个类只有一个Class对象,而且通过该类的所有实例对象都可以得到这个Class对象,反过来说通过Class对象我们也可以实例化对象;

      那么假如我们通过程序可以获取到java堆中的Class对象,那么我们不就可以自由的实例化对象,而不需要总是依靠new关键字了么!这就是所谓的java反射,而运行状态指的是该类的字节码文件必须加载并且在java堆中生成对应的Class对象!

      那么现在我们就要想办法从外部程序怎么获取这个Class对象,一般有三种方法,个人感觉对应于三个阶段比较好记一点,下图所示,有自己独特的记忆方法是最好的;

      我感觉最好把Class对象看作是student.java在内存中另外一种表现形式,这样你才能更好理解反射的各种用法。。。

    2.反射的简单使用

      我们既然得到了一个类的Class对象,这个Class对象中肯定包含了该类的属性和方法的所有信息,换句话说就是可以调用里面的各种方法(公共方法和私有方法)、获取修饰符、获取构造器、得到类名和方法名等等,简单列举一下最基本的方法:

      getName():获得类的完整名字。
      getFields():获得类的public类型的属性。
      getDeclaredFields():获得类的所有属性。包括private 声明的和继承类
      getMethods():获得类的public类型的方法。
      getDeclaredMethods():获得类的所有方法。包括private 声明的和继承类
      getMethod(String name, Class[] parameterTypes):获得类的特定方法,name参数指定方法的名字,parameterTypes 参数指定方法的参数类型。
      getConstructors():获得类的public类型的构造方法。
      getConstructor(Class[] parameterTypes):获得类的特定构造方法,parameterTypes 参数指定构造方法的参数类型。
      newInstance():通过类的不带参数的构造方法创建这个类的一个对象。

      基于这些方法我们下面我们就写个最简单的例子来使用一下这些方法;

    package com.wyq.day527;
    
    import java.lang.reflect.Constructor;
    import java.lang.reflect.Field;
    import java.lang.reflect.InvocationTargetException;
    import java.lang.reflect.Method;
    
    //这个类有私有属性,私有方法,公开属性,公开方法,无参构造器,有参构造器,get/set方法
    public class Student {
        private String name = "小花";
        public int age = 4;
        
        public Student() {
        }
        public Student(String name, int age) {
            this.name = name;
            this.age = age;
        }
        public void say(String man){
            System.out.println(man + "大声说话。。。");
        }
        private void listen(String man){
            System.out.println(man + "小声听歌");
        }
        @Override
        public String toString() {
            return "Student [name=" + name + ", 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 static void main(String[] args) throws NoSuchFieldException, SecurityException, InstantiationException, IllegalAccessException, NoSuchMethodException, IllegalArgumentException, InvocationTargetException {
            //获取Student的Class对象,后面的所有操作都是根据这个来的
            Class<Student> clazz = Student.class;
            
            //获取全类名
            String className = clazz.getName();
            System.out.println("1:"+className);
            
            //获取类中所有属性全名
            Field[] fields = clazz.getDeclaredFields();
            for (int i = 0; i < fields.length; i++) {
                System.out.println("2:"+fields[i]);
            }
            
            //获取类中所有方法全名
            Method[] methods = clazz.getDeclaredMethods();
            for (int i = 0; i < methods.length; i++) {
                System.out.println("3:"+methods[i]);
            }
            
            //根据空构造器来实例化对象,并调用其中的listen方法
            Student instance = clazz.newInstance();
            System.out.print("4:");
            instance.listen("小王");
            
            //首先通过空构造器实例对象,然后获取指定方法名(这里会指定方法参数类型),然后通过invoke方法来调用该指定方法
            //注意这种调用方法和上面这种的区别,好像是这种可以捕捉异常,更安全吧!
            Student instance3 = clazz.newInstance();
            Method method = clazz.getDeclaredMethod("say", String.class);
            System.out.print("5:");
            method.invoke(instance3, "张三");
            
            //调用空构造器实例对象,获取指定属性名,注意,假如该属性是私有的,一定要调用field.setAccessible(true),不然会报错
            //然后就是设置属性值
            Student instance4 = clazz.newInstance();
            Field field = clazz.getDeclaredField("age");
            field.setAccessible(true);    
            field.set(instance4, 20);
            System.out.println("6:"+instance4.getAge());
            
            //调用有参构造器传入参数来实例化对象,并调用其中的toString()方法
            Constructor<Student> constructor = clazz.getDeclaredConstructor(String.class,int.class);
            Student instance2 = constructor.newInstance("java小新人",18);
            System.out.println("7:"+instance2.toString());
        
        }
    }

      测试结果为:

       补充一点小东西:能不能直接通过反射实例化一个类的对象,然后去调用父类中的方法或属性呢?假如不用反射的话是可以直接调用父类的方法的,但是这里不能,所以我们可以想办法获取父类的Class对象,比如上面的Student类有个父类是Person类,那么可以通过

    Class clazz = Student.class;

    Class personclazz = clazz.getSuperclass();

      这样我们就得到了父类的Class对象了,然后就可以跟前面一样的用了,很简单吧!

    3.代理

      代理应该很熟悉了,大白话说就是中介,比如找工作、买房买车等,都可以找找中介,因为这样可以省很多时间;

      在java代码中的代理其实很容易,就是用一个代理类将目标类封装起来,我们调用代理类的方法就行了,不需要直接和目标类打交道,画个简单的图:可以看到代理类和目标类的方法名最好要一样,这样的好处就是我们使用代理类就和使用目标类一样;另外,我们可以在代理类的方法中再调用一下其他类的方法,这样做有个什么好处呢?可以实现给目标类扩展新功能而不需要改变目标类的代码(专业一点就叫做解耦合)

      在java中的代理分为两种,静态代理和动态代理:静态代理就是在源码阶段我们手动的写个代理类将目标类给包装起来,这种方式比较水,因为要自己写代码,最好可以自动生成这个代理类就最好了;于是就有了动态代理,动态代理就是在运行阶段有jvm自动生成这个代理类,我们直接用就好。显而易见,动态代理才是我们的主菜;

      下面就分别说说静态代理和动态代理:

      3.1.静态代理

      这个没什么好说的,我们看一个最简单的例子就一目了然了,在这里,我们要思考一下怎么包装目标类最好呢?我们最好可以让代理类和目标类都实现同一个接口,那么两个类的方法名就是一样的了,然后就是把目标类传入代理类中

      接口:

    package com.wyq.day527;
    
    public interface Animal {
        public void run();
        public void eat();
    
    }

      目标类:

    package com.wyq.day527;
    
    public class Dog implements Animal{
        
        @Override
        public void run() {
            System.out.println("狗----run");
        }
    
        @Override
        public void eat() {
            System.out.println("狗----eat");
        }
    
    }

      代理类及扩展Dog类中eat方法:

    package com.wyq.day527;
    
    public class DogAgent implements Animal{
        //这里就是将通过构造器传进来的目标类给保存起来
        private Dog dog;
        public DogAgent(Dog dog) {
            this.dog = dog;
        }
    
        @Override
        public void run() {
            dog.run();
        }
    
        @Override
        public void eat() {
            System.out.println("扩展------->这里可以进行日志或者事务处理。。。。。");
            dog.eat();
        }
        
        public static void main(String[] args) {
            Dog dog1 = new Dog();
            DogAgent agent = new DogAgent(dog1);
            //我们想对eat方法进行扩展,而不用修改Dog类中的源代码,直接在代理类中进行扩展即可
            agent.eat();
        }
    
    }

       

      测试结果如下,这样扩展起来很容易,而且对于那些不清楚源代码的程序员来说完全感觉不到Dog代理类的存在,还以为就是使用Dog类(在很多的框架中大量用到代理的这个思想)。。。

       3.2.动态代理

      静态代理有个很大的缺陷,就是代理类需要自己去写,假如实际项目中用到的类跟我们这里测试的一样的简单就好了,那自己写就自己写吧!然而实际中一个类中的方法可能有几十个几百个,来,你去试试写个代理类。。。简直坑爹,而且写的代码还都差不多,这就意味着又要为另外一个类写代理的时候再重复写一遍,简直太糟糕了!

      为了弥补这个缺陷,一些大佬就设计出了可以自动生成代理类的手段,这就很舒服了,这个手段是比较厉害的,但是有点儿不好理解,要仔细想想!而动态代理有两种方式,JDK动态代理和CGLib动态代理,下面说的是JDK动态代理。。。。

      首先JDK动态代理就不止有代理类和目标类了,还有一个中间类,这个中间类有什么用呢?我们可以画个图看看;

      上图可以简单的知道调用代理类中的所有方法实际上都是调用中间类的invoke方法,而在invoke方法中才是真正去调用对应的目标类的目标方法;这个比静态代理多了一层结构而已,好好理解一下还是很容易的。。。

      在这里java已经为我们提供了Proxy代理类了,我们可以看看这个类中主要的东西:有参构造是传递进去一个InvocationHandler类型的参数然后复制给属性h;然后就是一个方法,这个方法最主要的是其中的三个参数,第一个参数是类加载器,任意类加载器都行,通常用目标类的类加载器即可;第二个参数是目标类实现的接口,跟静态代理差不多,这里是为了让代理类和目标类的方法名一样;第三个参数是一个InvocationHandler类型的参数,注意,这个h是我们要自己写代码实现的,而不是属性中的那个h哦~~

      上面的InvocationHandler接口的实现类就是中间类,这个接口中只有一个invoke方法,我们可以用匿名类的形式,直接用new InvocationHandler(){重写invoke方法} 这种形式;

      废话不多说我们来看一个很简单的例子就知道了:

      接口:

    package com.wyq.day527;
    
    public interface Animal {
        public void run();
        public void eat();
    }

      目标类:

    package com.wyq.day527;
    
    public class Dog implements Animal{
        @Override
        public void run() {
            System.out.println("狗----run");
        }
        @Override
        public void eat() {
            System.out.println("狗----eat");
        }
    }

      代理类的使用以及测试结果;

    package com.wyq.day527;
    
    import java.lang.reflect.InvocationHandler;
    import java.lang.reflect.Method;
    import java.lang.reflect.Proxy;
    
    public class MyProxy {
        public static void main(String[] args) {
             //生成$Proxy0的class文件,也就是代理类的字节码文件
            System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
     
            Animal target = new Dog();
        //注意类加载器可以是任意一个类加载器,当然我们就随便用用目标类的类加载器了;获取目标类接口的方法就不多说了;
        //最主要的就是InvocationHandler中的invoke方法中的逻辑,想扩展什么就扩展什么
    Animal proxyDog
    = (Animal)Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(),new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("现在执行的所有方法都经过动态代理...."); method.invoke(target, args);//这里有没有很熟悉,不就是反射么。。。 return null; } }); proxyDog.run(); proxyDog.eat(); } }

    4.JDK动态代理源码

      在上面的代码中,有两个地方需要再仔细看看,第一个是生成的代理类的字节码文件,由于动态代理的代理类是动态生成的,我们有没有办法拿到其中的源码看看到底生成了一些什么东西呢?第二个就是invoke方法中用反射去调用目标类的方法,有没有觉得很奇怪那个参数method,args为什么这么神奇,刚好就是对应于目标类的方法名和方法形参呢?

      4.1.反编译代理类字节码文件

      要想知道这两个问题我们首先要拿到代理类的字节码,由于添加了获取代理类字节码文件的那行代码,我们可以在我们的电脑中找到代理类的字节码文件;

      基于eclispe:选中项目,右键,选择最后一个properties,就能看到项目路径了:

      

      然后进入到该路径下面,有个comsunproxy目录下:

      拿到了字节码文件,怎么变成源码文件呢?也就是变成xxx.java这样的,这里就用到一个小技巧,叫做反编译,我们可以下载一个小软件,下图所示:

      反编译软件百度云链接:https://pan.baidu.com/s/1czLYYC1Zij2LwQ3ES5fidg 提取码:d9a0 

     

      4.2.代理类源码

      为了代码简洁,这里我将一些不重要的代码进行删减,然后调整一下顺序:

    package com.sun.proxy;
    
    import com.wyq.day527.Animal;
    import java.lang.reflect.InvocationHandler;
    import java.lang.reflect.Method;
    import java.lang.reflect.Proxy;
    import java.lang.reflect.UndeclaredThrowableException;
    
    //注意这个代理类的名字$Proxy0,很奇怪的一个名字,也实现了Animal接口,而且还是继承Proxy这个类,前面我们对这个Proxy这个类简单的说了一下的,这里就会用到
    public final class $Proxy0 extends Proxy implements Animal{
      //此处这个静态代码块中就是我们熟悉的反射了,获取方法的Method对象,可以简单看作是获取方法的全名吧
      static{
          m1 = Class.forName("java.lang.Object").getMethod("equals", new Class[] { Class.forName("java.lang.Object") });
          m3 = Class.forName("com.wyq.day527.Animal").getMethod("run", new Class[0]);
          m4 = Class.forName("com.wyq.day527.Animal").getMethod("eat", new Class[0]);
          m2 = Class.forName("java.lang.Object").getMethod("toString", new Class[0]);
          m0 = Class.forName("java.lang.Object").getMethod("hashCode", new Class[0]);
          return;
    }
      //保存一下目标类中的方法,其中除了run()和eat()两个方法之外,还有equals、toString、hashCode方法,这三个默认都是要实现的
      private static Method m1;
      private static Method m3;
      private static Method m4;
      private static Method m2;
      private static Method m0;
      
    //这个有参构造将InvocationHandler参数传给父类保存起来,也就是那个父类树属性h,方便后面使用这个h
      public $Proxy0(InvocationHandler paramInvocationHandler){
        super(paramInvocationHandler);
      }
      
      public final boolean equals(Object paramObject){
          return ((Boolean)this.h.invoke(this, m1, new Object[] { paramObject })).booleanValue();
        
      }
      
      public final void run(){
      //注意,此处的invoke方法可不是反射哦!是调用父类中保存的属性h,其实就是中间类,调用这个类的invoke方法,好好看一下形参
      //this代表当前代理类,m3表示run方法的全类名,null其实就是run方法的参数,这里没有参数就是null
      //现在知道为什么在中间类的invoke方法中可以直接用反射了吧,因为目标类的方法的Method对象,目标类和方法参数都准备好了,不就可以用反射了么....
      //后面几个方法都差不多,就不说废话了
          this.h.invoke(this, m3, null);
          return;
      }
      
      public final void eat(){
          this.h.invoke(this, m4, null);
          return;
      }
      
      public final String toString(){
          return (String)this.h.invoke(this, m2, null);
      }
      
      public final int hashCode(){
          return ((Integer)this.h.invoke(this, m0, null)).intValue();
      }

     5.总结

      其实反射和动态代理还是很容易的,都是一些很基础的东西,再说一下用代理的好处,可以避免我们直接和目标类接触,实现解耦,而且有利于目标类的扩展,而且代理类用起来方式和目标类一样,所以我们在很多框架中即使用了代理,但是我们通常是感觉不出来的!打个比喻,就好像我们去餐馆吃饭,你觉得你是直接去厨房跟厨师说你要吃什么什么,而且别放辣......还是直接和服务员说这些要求比较好呢?差不多的道理吧!

      话说有个问题,上面的JDK动态代理必须要目标类要实现某一个或几个接口,假如我们的类没有实现接口怎么啊?这就日了狗了,于是就有了CGLib动态代理,这种代理方式刚好弥补了JDK动态代理的缺陷,其实就是生成一个目标类的子类,这个子类就是我们需要的代理类,重写一下父类的所有方法,那么代理类所有方法的名字就和目标类一样了,再然后就是反射调用父类的方法,前面说反射的最后那里好像说过了....后面有时间再简单说说CGLib动态代理吧!

      话说向进一步理解JDK动态代理的,可以去Proxy类中的newInstance方法中看看源码,应该就差不多了。。。。

  • 相关阅读:
    sublime text3快捷键
    SublimeText3追踪函数工具CTags设置及使用
    mysql的表锁
    ESXI vSphere如何设置虚拟机开机自启动
    gitee支持的开源许可证
    keepalive安装部署踩坑参考
    《牧神记》中诗词大全,有的可能是作者瞎编的
    定位Linux服务器SSH敲命令响应慢的问题
    Crypto-JS——Uncaught Error: Malformed UTF-8 data
    VUE——vue中组件之间的通信方式有哪些
  • 原文地址:https://www.cnblogs.com/wyq1995/p/10936286.html
Copyright © 2020-2023  润新知