Java基础知识(6)-- 反射与动态代理

一、反射

要想理解反射的原理,首先要了解什么是类型信息。Java让我们在运行时识别对象和类的信息,主要有2种方式:一种是传统的RTTI,它假定我们在编译时已经知道了所有的类型信息;另一种是反射机制,它允许我们在运行时发现和使用类的信息。

RTTI,即Run-Time Type Identification,运行时类型识别。RTTI能在运行时就能够自动识别每个编译时已知的类型。很多时候需要进行向上转型,比如Base类派生出Derived类,但是现有的方法只需要将Base对象作为参数,实际传入的则是其派生类的引用。那么RTTI就在此时起到了作用,比如通过RTTI能识别出Derived类是Base的派生类,这样就能够向上转型为Derived。类似的,在用接口作为参数时,向上转型更为常用,RTTI此时能够判断是否可以进行向上转型。

理解RTTI在Java中的工作原理,首先需要知道类型信息在运行时是如何表示的,这是由Class对象来完成的,它包含了与类有关的信息。Class对象就是用来创建所有“常规”对象的,Java使用Class对象来执行RTTI,即使你正在执行的是类似类型转换这样的操作。

每个类都有一个 Class对象,包含了与类有关的信息。当编译一个新类时,会产生一个同名的 .class 文件,该文件内容保存着 Class 对象。类加载相当于 Class 对象的加载,类在第一次使用时才动态加载到 JVM 中。当程序创建一个对类的静态成员的引用时,就会加载这个类。Class对象仅在需要的时候才会加载,static初始化是在类加载时进行的。也可以使用 Class.forName("com.mysql.jdbc.Driver") 这种方式来控制类的加载,该方法会返回一个 Class 对象。或者使用Base.class。注意,有一点很有趣,使用功能”.class”来创建Class对象的引用时,不会自动初始化该Class对象,使用forName()会自动初始化该Class对象。

为了使用类而做的准备工作一般有以下3个步骤:

加载:由类加载器完成,找到对应的字节码,创建一个Class对象

链接:验证类中的字节码,为静态域分配空间

初始化:如果该类有超类,则对其初始化,执行静态初始化器和静态初始化块


反射可以提供运行时的类信息,并且这个类可以在运行时才加载进来,甚至在编译时期该类的 .class 不存在也可以加载进来。

Class 和java.lang.reflect 一起对反射提供了支持,java.lang.reflect 类库主要包含了以下三个类:

Field:可以使用 get() 和 set() 方法读取和修改 Field 对象关联的字段;

Method:可以使用 invoke() 方法调用与 Method 对象关联的方法;

Constructor:可以用 Constructor 创建新的对象。


       反射最大的作用之一就在于我们可以不在编译时知道某个对象的类型,而在运行时通过提供完整的”包名+类名.class”得到。注意:不是在编译时,而是在运行时。

功能:

* 在运行时能判断任意一个对象所属的类

* 在运行时能构造任意一个类的对象

* 在运行时判断任意一个类所具有的成员变量和方法

* 在运行时调用任意一个对象的方法


应用场景:

反射技术常用在各类通用框架开发中。因为为了保证框架的通用性,需要根据配置文件加载不同的对象或类,并调用不同的方法,这个时候就会用到反射----运行时动态加载需要加载的对象。


  反射机制并没有什么神奇之处,当通过反射与一个未知类型的对象打交道时,JVM只是简单地检查这个对象,看它属于哪个特定的类。因此,那个类的.class对于JVM来说必须是可获取的,要么在本地机器上,要么从网络获取。所以对于RTTI和反射之间的真正区别只在于:

RTTI,编译器在编译时打开和检查.class文件

反射,运行时打开和检查.class文件


举例来说,假设我们为一个工厂编写生产汽车的类。那么,很明显的,汽车可能具有不同的类型。也就是说,在真正开始生产之前,我们无法确定采用哪种方式生产汽车。

假设生产不同类型汽车的部门都属于一个工厂旗下,那情况还比较容易处理一些。 例如,该工厂可以生产轿车和卡车,这两种类型的汽车的生产由同一工厂的不同部门负责。

public class Factory {

    voidproduce(ProduceDepartment pd) {

        pd.produce();

    }

}

interface ProduceDepartment {

    void produce();

}

class CarProduceDepartment implements ProduceDepartment {

    @Override

    public void produce() {

        System.out.println("轿车生产");

    }

}

class TruckProduceDepartment implements ProduceDepartment {

    @Override

    public void produce() {

       System.out.println("卡车生产");

    }

}

由于生产的部门都属于同一家工厂,虽然不确定在运行时的调用类型,但我们通过以上的代码编写方式,已经足够控制。但假设一下,假设汽车的生产由不同的生产厂商完成。也就是说,你根本无法确定在实际生产时,有哪些生产厂商。 对应在编程的思想中来说,也就是说,你在编写程序的时候,根本就不知道有哪些类型能够提供给你。

所以,情况就变得有些复杂了。这个时候,就是反射站出来装逼的时候。我们可以修改我们的代码如下:

public class Factory {

   @SuppressWarnings("unchecked")

    void produce(StringclassName) {

       Class clazzPS = null;

        try {

            //通过反射查询到对应的类的Class对象

            clazzPS =(Class) Class.forName(className);

        } catch(ClassNotFoundException e) {

           e.printStackTrace();

        }


        if (clazzPS != null){

            try {

                //通过Class对象生产我们要使用的常规对象

               ProduceStandard ps = clazzPS.newInstance();

               ps.produce(); //生产

            } catch(InstantiationException e) {

               e.printStackTrace();

            } catch (IllegalAccessExceptione) {

               e.printStackTrace();

            }

        }

    }

}


interface ProduceStandard {

    void produce();

}

这下牛逼了。实际我们做的工作原理仍然很简单,我声明生产汽车的规范(接口)。之后任何生产厂商都可以根据该规范去具体实现。我们在运行时通过类名(即生产厂商的实现)去查找到你的实现。然后通过newInstance创建可以使用的常规对象,从而来调用并进行生产。这就是所谓的反射。

相信到此你就明白了所谓的反射的原理,实际上反射在实际的编码工作中,最常见的就是驱动程序。最最最最最最最熟悉的,应该就是数据库驱动jdbc的使用了。以mysql的访问来说,我们在初始化驱动的时候,都会用到这样的代码:

 Class.forName("com.mysql.jdbc.Driver")

现在我们总算知道它的用意了。很显然,Java的设计者在设计的时候肯定不会知道之后具体有哪些数据库需要实现驱动。所以使用反射就是最合适的了。


反射的优点:

* 可扩展性 :应用程序可以利用全限定名创建可扩展对象的实例,来使用来自外部的用户自定义类。

* 类浏览器和可视化开发环境 :一个类浏览器需要可以枚举类的成员。可视化开发环境(如 IDE)可以从利用反射中可用的类型信息中受益,以帮助程序员编写正确的代码。

* 调试器和测试工具 : 调试器需要能够检查一个类里的私有成员。测试工具可以利用反射来自动地调用类里定义的可被发现的 API 定义,以确保一组测试中有较高的代码覆盖率。

 

反射的缺点:

尽管反射非常强大,但也不能滥用。如果一个功能可以不用反射完成,那么最好就不用。在我们使用反射技术时,下面几条内容应该牢记于心。

* 性能开销 :反射涉及了动态类型的解析,所以 JVM 无法对这些代码进行优化。因此,反射操作的效率要比那些非反射操作低得多。我们应该避免在经常被执行的代码或对性能要求很高的程序中使用反射。

* 安全限制 :使用反射技术要求程序必须在一个没有安全限制的环境中运行。如果一个程序必须在有安全限制的环境中运行,如 Applet,那么这就是个问题了。

* 内部暴露 :由于反射允许代码执行一些在正常情况下不被允许的操作(比如访问私有的属性和方法),所以使用反射可能会导致意料之外的副作用,这可能导致代码功能失调并破坏可移植性。反射代码破坏了抽象性,因此当平台发生改变的时候,代码的行为就有可能也随着变化。


二、动态代理

首先它是一个代理机制。我们知道,代理可以看做是对调用目标的一个包装,这样我们对目表代码的调用不是直接发生的,而是通过代理完成。通过代理可以让调用者与实现者直接解耦。代理的发展经历了静态到动态的过程,源于静态代理引入的额外工作。

所谓动态代理,就是实现阶段不用关心代理谁,而是在运行阶段才能指定代理哪个对象(不确定性)。如果是自己写代理类的方式就是静态代理(确定性)。

组成要素:

(动态)代理模式涉及的三个要素:

其一:抽象类的接口

其二:被代理类(具体实现抽象接口的类)

其三:动态代理类:实际调用被代理类的方法和属性的类


创建一个动态代理对象步骤,具体代码见后面:

创建一个InvocationHandler对象

//创建一个与代理对象相关联的InvocationHandler

InvocationHandlerstuHandler = new MyInvocationHandler(stu);


使用Proxy类的getProxyClass静态方法生成一个动态代理类stuProxyClass

Class stuProxyClass =Proxy.getProxyClass(Person.class.getClassLoader(), new Class[]{Person.class});


获得stuProxyClass 中一个带InvocationHandler参数的构造器constructor

Constructor constructor =PersonProxy.getConstructor(InvocationHandler.class);


通过构造器constructor来创建一个动态实例stuProxy

Person stuProxy = (Person) cons.newInstance(stuHandler);


上面四个步骤可以通过Proxy类的newProxyInstances方法来简化:

//创建一个与代理对象相关联的InvocationHandler

InvocationHandler stuHandler = newMyInvocationHandler(stu);

//创建一个代理对象stuProxy,代理对象的每个执行方法都会替换执行Invocation中的invoke方法

Person stuProxy= (Person)Proxy.newProxyInstance(Person.class.getClassLoader(), newClass[]{Person.class}, stuHandler);


完整的例子:

首先是定义一个Person接口:

 /**

 *创建Person接口

 * @author Gonjan

 */

public interface Person {

    //上交班费

    void giveMoney();

}


创建需要被代理的实际类:

public class Student implements Person {

    private String name;

    public Student(Stringname) {

        this.name = name;

    }


    @Override

    public void giveMoney(){

        try {

          //假设数钱花了一秒时间

            Thread.sleep(1000);

        } catch(InterruptedException e) {

            e.printStackTrace();

        }

       System.out.println(name + "上交班费50元");

    }

}


再定义一个检测方法执行时间的工具类,在任何方法执行前先调用start方法,执行后调用finsh方法,就可以计算出该方法的运行时间,这也是一个最简单的方法执行时间检测工具。

public class MonitorUtil {

    private staticThreadLocal tl = new ThreadLocal<>();


    public static voidstart() {

       tl.set(System.currentTimeMillis());

    }


    //结束时打印耗时

    public static voidfinish(String methodName) {

        long finishTime =System.currentTimeMillis();

       System.out.println(methodName + "方法耗时" + (finishTime - tl.get()) + "ms");

    }

}


创建StuInvocationHandler类,实现InvocationHandler接口,这个类中持有一个被代理对象的实例target。InvocationHandler中有一个invoke方法,所有执行代理对象的方法都会被替换成执行invoke方法。

public class StuInvocationHandler implementsInvocationHandler {

   //invocationHandler持有的被代理对象

    T target;

    publicStuInvocationHandler(T target) {

       this.target = target;

    }


    /**

     * proxy:代表动态代理对象

     * method:代表正在执行的方法

     * args:代表调用目标方法时传入的实参

     */

    @Override

    public Objectinvoke(Object proxy, Method method, Object[] args) throws Throwable {

       System.out.println("代理执行" +method.getName() + "方法");

     */  

        //代理过程中插入监测方法,计算该方法耗时

        MonitorUtil.start();

        Object result =method.invoke(target, args);

       MonitorUtil.finish(method.getName());

        return result;

    }

}


做完上面的工作后,我们就可以具体来创建动态代理对象了,上面简单介绍了如何创建动态代理对象,我们使用简化的方式创建动态代理对象:

public class ProxyTest {

    public static voidmain(String[] args) {

        //创建一个实例对象,这个对象是被代理的对象

        Person zhangsan = new Student("张三");


        //创建一个与代理对象相关联的InvocationHandler

        InvocationHandler stuHandler = newStuInvocationHandler(zhangsan);


        //创建一个代理对象stuProxy来代理zhangsan,代理对象的每个执行方法都会替换执行Invocation中的invoke方法

        Person stuProxy = (Person)

Proxy.newProxyInstance(Person.class.getClassLoader(), new

Class<?>[]{Person.class}, stuHandler);


       //代理执行上交班费的方法

       stuProxy.giveMoney();

    }

}

生成的代理类:$Proxy0 extends Proxy implements Person,我们看到代理类继承了Proxy类,所以也就决定了java动态代理只能对接口进行代理,Java的继承机制注定了这些动态代理类们无法实现对class的动态代理。

首先,实现对应的InvocationHandler,然后,以接口Person为纽带,为被调用目标构建代理对象,进而应用程序就可以使用代理对象间接运行调用目标的逻辑,代理为应用插入额外逻辑(这里是计算耗时的逻辑)提供了便利的入口。通过InvocationHandler接口,所有方法都由该Handler来进行处理,即所有被代理的方法都由InvocationHandler接管实际的处理任务。此外,我们常可以在invoke方法实现中增加自定义的逻辑实现,实现对被代理类的业务逻辑无侵入。

如果被调用者没有实现接口,而我们还是希望利用动态代理机制,那么可以考虑其他方式是。我们知道Spring AOP支持两种模式的动态代理,JDK Proxy或者cglib,如果我们选择cglib方式,你会发现对接口的依赖被克服了。cglib动态代理采取的是创建目标类的子类的方式,因为是子类化,我们可以达到近似使用被调用者本身的效果。


JDK Proxy的优势:

* 最小化依赖关系,减少依赖意味着简化开发和维护,JDK本身的支持,可能比cglib更加可靠

* 平滑进行JDK版本升级,而字节码类库通常需要进行更新以保证在新版Java上能够使用

* 代码实现简单


cglib框架的优势:

* 有时候调用目标可能不便实现额外接口,从某种角度看,限定调用者实现接口是有些侵入性的实践,类似cglib动态代理没有这种限制

* 只操作我们关心的类,而不必为其他相关类增加工作量

* 高性能


三、动态编译

对有些应用来说,Java源代码的内容在运行时刻才能确定。这个时候就需要动态编译源代码来生成Java字节代码,再由JVM来加载执行。典型的场景是很多算法竞赛的在线评测系统(如PKU JudgeOnline),允许用户上传Java代码,由系统在后台编译、运行并进行判定。在动态编译Java源文件时,使用的做法是直接在程序中调用Java编译器。  

动态编译的流程:

1)生成编译器

  JavaCompiler compiler =ToolProvider.getSystemJavaCompiler();


2)选择使用哪种编译方式,一种是直接使用compiler的run方法,如下:

    //JavaCompiler最核心的方法是run, 通过这个方法编译java源文件, 前3个参数传null时,

    //分别使用标准输入/输出/错误流来 处理输入和编译输出. 使用编译参数-d指定字节码输出目录.

    int compileResult =javac.run(null, null, null, "-d", distDir.getAbsolutePath(),javaFile.getAbsolutePath());

    //run方法的返回值: 0-表示编译成功, 否则表示编译失败

    if(compileResult != 0) {

       System.err.println("编译失败!!");

        return;

    }


 还有一种方法是compiler的getTask方法获取CompilationTask,然后调用CompilationTask的call方法执行编译任务,如下:

 Boolean result =compiler.getTask(null, javaFileManager, collector, options, null,Arrays.asList(javaFileObject)).call();

 if(!result) {

       System.err.println("编译失败!!");

        return;

 }

 其中,

 DiagnosticCollectorcollector = new DiagnosticCollector<>();

 JavaFileManagerjavaFileManager = newMyJavaFileManager(compiler.getStandardFileManager(collector, null, null));

 List options= new ArrayList<>();

 options.add("-target");

 options.add("1.8");

 JavaFileObjectjavaFileObject = new MyJavaFileObject(cls, code);


3)利用反射调用动态类的方法

ClassLoader classloader = new MyClassLoader();

Class clazz = null;

try {

    clazz  = classloader.loadClass(cls);

} catch (ClassNotFoundException e) {

    e.printStackTrace();

}


Method method = null;

try {

    method =clazz.getMethod("hello");

} catch (NoSuchMethodException e) {

    e.printStackTrace();

}

try {

   method.invoke(clazz.newInstance());

} catch (IllegalAccessException e) {

    e.printStackTrace();

} catch (InvocationTargetException e) {

    e.printStackTrace();

} catch (InstantiationException e) {

    e.printStackTrace();

}

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,236评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,867评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,715评论 0 340
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,899评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,895评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,733评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,085评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,722评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,025评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,696评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,816评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,447评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,057评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,009评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,254评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,204评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,561评论 2 343

推荐阅读更多精彩内容