这一篇咱们说说反射和动态代理,为何这两个要一块儿说呢?由于动态代理中会用到反射,并且java中反射的用处太多了,基本上无处不在,并且功能十分强大;java
1.反射简介程序员
反射是什么呢?通常都是很专业的说法:在运行状态中,对于任意一个类,都可以知道这个类的全部属性和方法;我最初看这句话我是没看出来什么厉害的地方,运行状态?什么是运行状态啊?安全
简单看看下面这个图,类的加载机制之前说过了,这里就随意看看,一个类的字节码文件经过类加载器加载到jvm的堆中最终会生成一个该类的Class对象,注意,一个类只有一个Class对象,并且经过该类的全部实例对象均可以获得这个Class对象,反过来讲经过Class对象咱们也能够实例化对象;框架
那么假如咱们经过程序能够获取到java堆中的Class对象,那么咱们不就能够自由的实例化对象,而不须要老是依靠new关键字了么!这就是所谓的java反射,而运行状态指的是该类的字节码文件必须加载而且在java堆中生成对应的Class对象!jvm
那么如今咱们就要想办法从外部程序怎么获取这个Class对象,通常有三种方法,我的感受对应于三个阶段比较好记一点,下图所示,有本身独特的记忆方法是最好的;ide
我感受最好把Class对象看做是student.java在内存中另一种表现形式,这样你才能更好理解反射的各类用法。。。测试
2.反射的简单使用this
咱们既然获得了一个类的Class对象,这个Class对象中确定包含了该类的属性和方法的全部信息,换句话说就是能够调用里面的各类方法(公共方法和私有方法)、获取修饰符、获取构造器、获得类名和方法名等等,简单列举一下最基本的方法:spa
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,就能看到项目路径了:
而后进入到该路径下面,有个com\sun\proxy目录下:
拿到了字节码文件,怎么变成源码文件呢?也就是变成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方法中看看源码,应该就差很少了。。。。