15.5 动态代理
在第6章(面对对象高级篇--抽象类与接口的应用)中讲过代理机制的操作,但是所讲解的代理设计属于静态代理。因为每一个代理类只能为一个接口服务,这样一来程序开发中必然会产生过多的代理类。为了避免这一问题,最好的做法就是通过一个代理类完成全部的代理功能,那么,此时就必须用到动态代理来完成了。
在Java中要想实现动态代理机制,需要用到java.lang.reflect.Proxy类、java.lang.reflect.InvocationHandler接口的支持!!
//InvocationHandler接口的定义
public interface InvocationHandler{
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable
}
在InvocationHandler接口中只定义了一个invoke()方法,此方法中的3个参数含义:
- Object proxy : 被代理的对象。代表动态代理对象
- Method method : 要调用的方法。 代表正在执行的方法
- Object[] args : 方法调用时所需要的参数 。代表调用目标方法时传入的实参
Proxy类提供了用于创建动态代理类和代理对象的静态方法, 它也是所有动态代理类的父类。如果在程序中为一个或多个接口动态地生成实现类,就可以使用Proxy类来创建动态代理类;如果需要为一个或多个接口动态地创建实例,也可以使用Proxy来创建动态代理实例。
(一)通过Proxy类创建动态代理对象 (当执行动态代理对象里的方法时,实际上会替换成调用InvocationHandler对象的invoke方法)
//Proxy类创建动态代理对象的方法,该代理对象的实现类实现了interfaces指定的系列接口,执行代理对象的每个方法时都会被替换执行InvocationHandler对象的invoke方法
public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h) throws IllegalArgumentException
参数含义:
- ClassLoader loader: 类加载器
- Class<?>[] interfaces: 得到全部的接口
- InvocationHandler h: 得到InvocationHandler接口的子类实例
在Java中主要有以下3种类加载器:
- Bootstrap ClassLoader: 此加载器采用C++编写,一般开发中是看不到的
- Extension ClassLoader: 用来进行扩展类的加载;一般对应的是jre\lib\ext目录中的类
- AppClassLoader: 加载classpath指定的类,是最常使用的一种加载器
范例: 取得类加载器
class Person{
}
public class ClassLoaderDemo{
public static void main(String args[]){
Person per = new Person();
System.out.println("类加载器:" + per.getClass().getClassLoader().getClass().getName());
}
}
运行结果:
类加载器:sun.misc.Launcher$AppClassLoader
从运行结果可以看出,默认的ClassLoader是AppClassLoader。在开发中,可以通过继承ClassLoader类来实现自己的类加载器。但是这样做的意义不大。
(二)创建一个动态代理类所对应的Class对象
//通过Proxy类创建一个动态代理类所对应的Class对象
public static Class<?> getProxyClass(ClassLoader loader, Class<?>... interfaces) throws IllegalArgumentException
参数含义:
- ClassLoader loader: 定义代理类的类加载器
- Class<?>... interfaces: 代理类实现的接口列表
完成动态代理操作实例:
1, 定义一个InvocationHandler接口的子类,主要作用是完成代理的具体操作~~~
接口InvocationHandler中只有一个抽象方法invoke()。。。所有实现InvocationHandler接口的类都必须实现该方法
package org.forfan06.dynaproxydemo;
import java.lang.reflect.Proxy;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
public class MyInvocationHandler implements InvocationHandler{
private Object obj; //真实的主体
public Object bind(Object obj){ //绑定真实操作主体
this.obj = obj;
return Proxy.newProxyInstance(obj.getClass().getClassLoader(), obj.getClass().getInterfaces(), this);
}
//动态调用方法invoke()
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable{
Object temp = method.invoke(this.obj, args); //调用方法,传入真实主体和参数
return temp; //返回方法的返回信息
}
}
在MyInvocationHandler类的bind()方法中接受被代理对象的真实主体实现,之后覆写InvocationHandler接口中的invoke()方法,完成具体的方法调用。
2, 定义接口
package org.forfan06.dynaproxydemo;
public interface Subject{
public String say(String name, int age);
}
3,定义真实主体实现类
package org.forfan06.dynaproxydemo;
public class RealSubject implements Subject{
public String say(String name, int age){
return "姓名:" + name + ";年龄:" + age;
}
}
第1,2,3步定义了接口及真实主体类,这样在操作时直接将真实主体类的对象传入到MyInvocationHandler类的bind()方法中即可。
4,测试动态代理:
package org.forfan06.dynaproxydemo;
public class DynaProxyDemo{
public static void main(String args[]){
MyInvocationHandler handler = new MyInvocationHandler(); //实例化代理操作类
Subject sub = (Subject) handler.bind(new RealSubject()); //绑定对象
String info = sub.say("forfan06", 27); //通过动态代理调用方法
System.out.println(info);
}
}
总结:从上面的程序看来,动态代理感觉与静态代理操作没什么不同,操作也比较复杂。 实际上,在普通编程过程中,确实无须用到动态代理,但在编写框架或底层基础代码时,动态代理的作用就非常大!!!!
=====================================动态代理和AOP==============================================
当程序通过反射方式为指定接口生成系列动态代理对象时,这些动态代理对象的实现类实现了一个或多个接口,动态代理对象就需要实现一个或多个接口里定义的所有方法。但问题是: 系统怎么知道如何实现这些方法??这个时候就轮到InvocationHandler对象登场了 --- 当执行动态代理对象里的方法时,实际上会替换成调用InvocationHandler对象的invoke方法。
只要我们开发一个实际使用的软件系统,就总会存在相同代码段重复出现的情况,在这种情况下,(为了方便讲解,我们准备代码段1、代码段2、代码段3、重复代码段)
- 对于刚从事软件开发的人而言,他们的做法是: 选择那些代码,一路“复制”、“粘贴”,立即实现了系统功能。但是,如果有一天需要修改重复代码段,则意味着每个源代码(代码段1、代码段2、代码段3)都需要进行修改,那么此段代码的修改、维护工作量将非常大!!!
- 大部分有经验的开发着会将重复代码段定义成一个方法,然后让代码段1、代码段2、代码段3都直接调用该方法即可。那么如果需要修改公用的代码段,只需要修改一个地方就可以了。而不需要去修改调用该方法的主体。 这种方式大大降低了软件后期维护的复杂度。。。。。但是这种方式依然产生了一个重要的问题:代码段1、代码段2、代码段3和重复代码段是分离开了, 但是代码段1、代码段2、代码段3又与一个特定的方法耦合了!!
- 最理想的效果是:代码段1、代码段2、代码段3既可以执行“重复代码段”,又无须在程序中以硬编码方式直接调用“重复代码段”方法,这时就可以通过动态代理来达到这种效果
由于JDK动态代理只能为接口创建动态代理,所以下面先提供一个Dog接口,在该接口中只定义了两个抽象方法。
public interface Dog{
public void info(); //info方法声明
public void run(); //run方法声明
}
上面接口里只是简单地定义了两个方法,并没有提供方法的实现,如果此时我们直接使用Proxy类为该接口创建动态代理对象,则动态代理对象的所有方法的执行效果又将完全一样。 在这种情况,我们先为该接口提供一个简单的实现类: GunDog
public class GunDog implements Dog{
//info方法的实现,仅仅打印一个字符串
public void info(){
System.out.println("我是一个猎狗");
}
//run方法的实现
public void run(){
System.out.println("我奔跑迅速");
}
}
Dog接口的实现类仅仅是为每个方法提供了一个简单实现。
我们需要实现的功能:让代码段1、代码段2和代码段3既可以执行重复代码段,又无须在程序中以硬编码方式直接调用重复代码段的方法。此时我们假设info()、run()两个方法代表代码段1、代码段2,那么要求:程序执行info()、run()方法时能调用某个通用方法,但是又不想以硬编码方式调用该方法。
下面提供一个DogUtil类,该类中包含两个通用方法:
public class DogUtil{
//第一个拦截器方法
public void method1{
System.out.println("=====模拟第一个通用方法=====");
}
//第二个拦截器方法
public void method2{
System.out.println("=====模拟通用方法二=====");
}
}
借助于Proxy和InvocationHandler就可以实现 ---- 当程序调用info()、run()方法时,系统可以 “自动” 将method1()和method2()两个通用方法插入info()、run()方法中执行。
此时,程序的关键在于下面的InvocationHandler接口的实现类:MyInvocationHandler类,该实现类的invoke()方法将会作为代理对象的方法实现
public class MyInvocationHandler implements InvocationHandler{
//需要被代理的对象
private Object target;
public void setTarget(Object target){
this.target = target;
}
//执行动态代理对象的所有方法时,都会被替换成执行如下的invoke方法
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable{
DogUtil du = new DogUtil();
//执行DogUtil对象中的method1方法
du.method1();
//以target作为主调来执行method方法
Object result = method.invoke(target, args);
//执行DogUtil对象中的method2方法
du.method2();
return result;
}
}
上面程序实现invoke()方法时包含了一行关键代码(Object result = method.invoke(target, args);) ,这行代码通过反射以target作为主调来执行method方法,这就回调了target对象的原有方法。 在这行代码之前调用DogUtil对象的method1()方法,在其后调用了DogUtil对象的method2()方法。
下面再为程序提供一个MyProxyFactory类,该对象专为指定的target生成动态代理实例。
public class MyProxyFactory{
//为指定的target生成动态代理对象
public static Object getProxy(Object target) throws Exception{
//创建一个MyInvocationHandler对象
MyInvocationHandler handler = new MyInvocationHandler();
//为MyInvocationHandler设置target对象
handler.setTarget(target);
//创建并返回一个动态代理
return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), handler);
}
}
上面的动态代理工厂类提供了一个getProxy()方法,该方法为target对象生成一个动态代理对象,这个动态代理对象与target实现了相同的接口,所以具有相同的public方法 --- 从这个意义上来看,动态代理对象可以当成target对象使用。当程序调用动态代理对象的指定方法时,实际上将变为执行MyInvocationHandler对象的invoke()方法。 例如,调用动态代理对象的info()方法,程序将开始执行invoke()方法,其执行步骤如下:
- 创建DogUtil实例
- 执行DogUtil实例的method1()方法
- 使用反射以target作为调用者执行info()方法
- 执行DogUtil实例的method2()方法
从以上执行过程,可以发现:当使用动态代理对象来替换target对象时,代理对象的方法就实现了前面的要求 --- 程序执行info()、run()方法时既能 “插入” method1()、method2()通用方法,但GunDog()方法中又没有以硬编码方式调用method1()和method2()方法。
下面提供一个主程序来测试这种动态代理的效果
public class Test{
public static void main(String args[]) throws Exception{
//创建一个原始的GunDog对象,作为target
Dog target = new GunDog();
//以指定的target来创建动态代理对象
Dog dog = (Dog)MyProxyFactory.getProxy(target);
dog.info();
dog.run();
}
}
上面程序中的dog对象实际上是动态代理对象,只是该动态代理对象也实现了Dog接口,所以也可以当成Dog对象使用。程序执行dog的info()、run()方法时,实际上会先执行DogUtil的method1()方法,再执行target对象的info()、run()方法,最后执行DogUtil的method2()方法。
**********采用动态代理可以非常灵活地实现解耦。通常而言,当我们使用Proxy生成一个动态代理时,往往不会凭空产生一个动态代理,这样没有太大的实际意义。通常都是为指定的目标对象生成动态代理**********
这种动态代理在AOP(Aspect Orient Programming,面向切面编程)中被称为AOP代理,AOP代理可代替目标对象,AOP代理包含了目标对象的全部方法。但AOP代理中的方法与目标对象的方法存在差异:AOP代理里的方法可以在执行目标方法之前、之后插入一些通用处理!!!!!!!!!!!!
补充一个实例: 使用Proxy和InvocationHandler来生成动态代理对象。
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
interface Person{
void walk();
void sayHello(String name);
}
class MyInvocationHandler implements InvocationHandler{
/*
执行动态代理对象的所有方法时,都会被替换成执行如下的invoke方法
其中:
proxy: 代表执行代理对象
method: 代表正在执行的方法
args: 代表调用目标方法时传入的实参
*/
public Object invoke(Object proxy, Method method, Object[] args){
System.out.println("------正在执行的方法:" + method);
if(args != null){
System.out.println("下面是执行该方法时传入的实参为: ");
for(Object val:args){
System.out.println(val);
}
}else{
System.out.println("调用该方法没有实参!");
}
return null;
}
}
public class ProxyTest{
public static void main(String args[]) throws Exception{
//创建一个InvocationHandler对象
InvocationHandler handler = new MyInvocationHandler();
//使用指定的InvocationHandler来生成一个动态代理对象
Person p = (Person) Proxy.newProxyInstance(Person.class.getClassLoader(), new Class[]{Person.class}, handler);
//调用动态代理对象的walk()、sayHello()方法
p.walk();
p.sayHello("forfan06");
}
}
运行结果:
------正在执行的方法:public abstract void Person.walk()
调用该方法没有实参!
------正在执行的方法:public abstract void Person.sayHello(java.lang.String)
下面是执行该方法时传入的实参为:
forfan06
以上程序首先提供了一个Person接口,该接口中包含了walk()和sayHello()两个抽象方法,接着定义了一个简单的InvocationHandler实现类MyInvocationHandler,定义该实现类时需要覆写invoke()方法 -- 调用代理对象的所有方法时都会被替换成调用此invoke()方法。该方法的三个参数解释如下:
proxy: 代表动态代理对象
method: 代表正在执行的方法。 例如 上面的程序就是 walk()方法。一般会在invoke()里面增加一些通用方法(譬如验证用户合法性什么的),也要包括调用目标方法的语句(用method.invoke(proxy,args)
args: 代表调用目标方法时传入的实参
从程序结果来看,不管程序是执行代理对象的walk()方法,还是执行代理对象的sayHello()方法,实际上都是执行了InvocationHandler对象的invoke()方法!!!!!!!!!
=====================================动态代理和AOP==============================================
15.6 类的生命周期
在一个类编译完成之后,下一步就要开始使用类。如果要使用一个类,肯定离不开JVM。在程序执行中JVM通过加载、链接、初始化3个步骤完成。类的加载就是通过类加载其把 .class 二进制文件装入JVM的方法区,并在堆区创建描述该类的java.lang.Class对象,用来封装数据。 需要注意的是,同一个类只会被JVM加载一次。链接就是把二进制数据组装成可以运行的状态。
链接分为校验、准备和解析3个步骤。校验用来确认此二进制文件是否适合当前的JVM(版本);准备就是为静态成员分配内存空间,并设置默认值;解析指的是转换常量池的代码引用为直接引用的过程,直到所有的符号引用都可以被运行程序使用(建立完整的对应关系)。 完成之后,类型即可初始化,初始化之后类的对象就可以正常地使用,直到一个对象不再被使用之后,将被垃圾回收,释放空间。当没有任何引用指向Class对象时将会被卸载,结束类的生命周期。
15.7 工厂设计模式
15.7.1 将反射应用在工厂模式上
工程设计模式在实际的开发中使用非常多。之前也讲过简单的工厂模式,通过简单的工厂设计模式可以达到类的解耦合目的,但是,也存在一个重要的问题:在增加一个子类时都需要修改工厂类,这样代码的修改和维护会很麻烦。学习了反射机制后,就可以通过发射机制来改善工厂类,让其在增加子类时可以不用做任何的修改,就可以达到功能的扩充。
(一)使用反射完成工厂设计
package org.forfan06.factorydemo;
interface Fruit{
public void eat();
}
class Apple implements Fruit{
public void eat(){
System.out.println("**吃苹果**");
}
}
class Orange implements Fruit{
public void eat(){
System.out.println("**吃橘子**");
}
}
class Factory{
public static Fruit getInstance(String className){
Fruit fruit = null;
try{
fruit = (Fruit) Class.forName(className).newInstance();
}catch(ClassNotFoundException e){
e.printStackTrace();
}
return fruit;
}
}
public class FactoryDemo01{
public static void main(String args[]){
//通过工厂类取得接口实例,传入完整的包.类名称
Fruit f = Factory.getInstance("org.forfan06.factorydemo.Apple");
if(f != null){
f.eat();
}
}
}
此时,上面的工厂操作类中使用了反射操作来取得Fruit实例,这样无论增加多少个子类,工厂类都不用去做任何修改。
15.7.2 结合属性文件的工厂模式
上面操作代码虽然通过反射取得了接口的实例,但是在操作时还是需要传入完整的 包.类名称,而且用户也无法知道一个接口有多少个可以使用的子类。
所以此时可以通过属性文件的形式配置所要的子类信息
(一)范例:属性文件fruit.properties
apple = org.forfan06.factorydemo.Apple
orange = org.forfan06.factorydemo.Orange
在属性文件中使用apple和orange表示完整的包.类名称,这样在使用时可以直接通过属性名称即可。
(二)范例: 属性操作类
class Init{
public static Properties getPro(){
Properties pro = new Properties();
File file = new File("D:" + File.separator + "fruit.properties");
try{
if(file.exists()){
pro.load(new FileInputStream(file));
}else{
pro.setProperty("apple", "org.forfan06.factorydemo.Apple");
pro.setProperty("orange", "org.forfan06.factorydemo.Orange");
pro.store(new FileOutputStream(f), "FRUIT CLASS");
}
}catch(){
e.printStackTrace();
}
return pro;
}
}
此类的主要功能是取得属性文件中的配置信息。如果属性文件不存在,则创建一个新的,并设置默认值
(三)测试程序
public class FactoryDemo02{
public static void main(String args[]){
Properties pro = Init.getPro();
Fruit f = Factory.getInstance(pro.getProperty("apple"));
if(f != null){
f.eat();
}
}
}
通过工厂类取得接口实例时,直接输入属性的key就可以找到其完整的包.类名称,以达到对象的实例话功能。
(四)总结
在本程序中可以发现,程序很好地实现了代码与配置文件的分离。通过配置文件配置要使用的类,之后通过程序读取配置文件,完成具体的功能。
当然,这些程序完成的前提是基于接口,所以接口在实际的开发中用处是最大的!!!
15.8 本章要点
- Class类是反射机制操作的源头
- Class类的对象有3种实例化方式:(1)通过Object类中的getClass()方法;(2)通过 “ 类.class ” 的形式获取Class类的实例化对象;(3)通过Class.forName()方法,此种方法最为常用。
- 可以通过Class类中的newInstance()方法进行对象的实例化操作,但是要求类中必须要存在无参构造方法。如果类中没有无参构造方法,则必须使用Constructor类来完成对象的实例化操作(Constructor类中的newInstance()方法)
- 可以通过反射取得一个类所继承的父类、实现的接口、类中的全部构造方法、全部普通方法、全部属性
- 使用反射机制可以通过Method调用类中的方法;也可以通过Field类直接操作类中的属性
- 动态代理可以解决开发中代理类过多的问题,提供统一的代理功能实现
- 在程序开发中使用反射机制并结合属性文件,可以达到代码与配置文件分离的目的。
15.9 习题
定义一个学生类,其中包含姓名、年龄、成绩的属性。之后由键盘输入学生的内容,并将内容保存在文件中。所有的操作要求全部使用反射机制完成,即不能使用通过关键字new创建学生类对象的操作。