jdk动态代理

本文对比分析了静态代理与动态代理的区别与应用场景。通过具体示例介绍了如何实现这两种代理模式,探讨了它们在封装原有代码调用及提供额外操作方面的优势。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

定义

   代理的使用场景很多,通常表现为封装原有代码的调用,提供额外的操作代码,或者说隐藏原始代码,控制对原始代码的访问,常见的用途例如,执行方法之前的权限检查,或者进行日志记录、事务处理等。

这里以权限检查为例,代理中增加的权限检查代码称之为操作方法,被代理对象成为委托对象

示例

package test;
public interface User {
	void addUser();
}

User接口,只有一个addUser方法

package test;

public class UserImp implements User {
	@Override
	public void addUser() {
		System.out.println("User add");
	}
}

实现了User接口的类

package test;

public class UserProxy implements User {
	
	private User user;	//存在对委托对象的引用
	public UserProxy(User user) {	//在构造方法中配置委托对象
		super();
		this.user = user;
	}

	@Override
	public void addUser() {
		System.out.println("access control");//这里就是操作方法,即权限检查代码
		user.addUser();//执行委托对象的方法
	}
	
}

   这里就是一个典型的静态代理示例,方式也就是提供一个User的实现类UserProxy,UserProxy类也就是静态代理模式中的代理类,类中存在一个委托类UserImp的实例对象引用,由此也可以看出静态代理的作用方式:

   提供满足User接口统一规范的代理类作为实现类,这里代理类与委托类实现相同的接口,目的在于屏蔽方法调用上的不同,即对外提供一致性访问(其实使用继承委托类的方式来重写委托类的方法也可实现相同的效果,不过考虑到耦合程度,所以代理以聚合方式实现)。这里为了简写,所以权限检查代码,即代理类提供的操作代码直接以一个输出表示。

   观察以上代码可以发现,代理类提供权限检查的方式,是将权限检查的操作代码放在统一访问方法,即addUser方法调用中,执行完权限检查再调用委托对象的addUser方法。这就是典型的静态代理实现方式,不过看完其实现方式,发现其中存在一个问题,问题就是将权限检查代码放入了addUser方法中,换句话说也就是将权限检查代码与User接口单独绑定到了一起,这时候如果还有其他的接口,在这些接口方法中也需要进行权限检查,那么可以想象,如果存在六十个接口,每个接口中方法都需要进行权限检查,则需要写出六十个代理类,而悲哀的地方就在于,这六十个代理类执行的操作代码(权限检查)都是相同的,那么到了这里,就可以想象,如果能把操作代码提取出来,使用这样的一份操作代码作用于六十个接口,即只需要写一份代码而非六十个代理类,这样该多好。

   这里就轮到动态代理出场了,提取出来的这份代码就是实现了InvocationHandler接口的类,这里不妨称之为UserHandler,这里就可以发现一个有趣的现象,为了不重复写代理类,所以不能将操作代码与某一个接口(例如User)单独绑定,所以这里操作代码没有实现任何一个原本静态代理中需要实现的接口,反而实现了InvocationHandler接口,将操作代码放在了InvocationHandler接口中唯一一个方法invoke方法中。

   虽然是动态代理,但是也不能脱离代理的本质,即执行了操作代码后也需要调用委托对象的方法,所以这里的UserHandler类中需要像静态代理一样的,保存一个委托类实例对象的引用,并在invoke方法中执行完操作代码后,调用委托对象的相应方法。

package test;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

public class UserHandler implements InvocationHandler {

	Object obj;//委托类实例对象的引用
		
	public UserHandler(Object obj) {//通过构造方法注入委托类实例对象
		super();
		this.obj = obj;
	}

	@Override
	public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
		System.out.println("access control");//操作代码,即权限检查代码
		Object result=method.invoke(obj, args);//操作代码之后,调用委托对象的相应方法
		return result;
	}

}

   以上代码就是一个典型的实现了InvocationHandler接口的类结构(invoke方法参数稍后再说),单独观察这个UserHandler发现,如果只是这样,那么这个UserHandler类跟之前提到的静态代理类是一毛一样的,都是存在一个委托类实例对象,都是在方法中执行检查代码,然后调用委托类实例对象的方法,为了观察清楚,这里看下静态代理的代理类内容:

package test;

public class UserProxy implements User {
	
	private User user;	//存在对委托对象的引用
	public UserProxy(User user) {	//在构造方法中配置委托对象
		super();
		this.user = user;
	}

	@Override
	public void addUser() {
		System.out.println("access control");//这里就是操作方法,即权限检查代码
		user.addUser();//执行委托对象的方法
	}
	
}

   观察以上动态代理中实现了InvocationHandler接口的UserHandler类和静态代理中实现了User接口的UserProxy类,(1).很明显的一个不同之处就是对于委托类实例对象的引用,UserProxy中是一个特定类型,即绑定了特定的接口类型的对象引用,而UserHandler中是一个泛型(姑且将Object称之为泛型)对象引用;

(2).其次就是对于委托对象方法的调用,UserProxy中既然是特定类型的委托对象,方法调用上也是指明了方法名称,方法的调用者的类型;UserHandler中则既没有指明方法名称,也没有指明方法的调用者的类型。其实这里方法调用者类型也就是(1)中刚提到的委托对象类型

   那么这里就可以总结出两者的不同:UserProxy代理类中因为所代理的委托对象是已经确定了类型的,并且委托对象的方法的调用也是确定了的,所以说UserProxy已经与特定类型的接口进行了绑定,不能发生改变(例如更换一个委托对象),所以属于静态代理。而UserHandler中保存的代理类实例对象引用并没有限制于特定类型,并且invoke方法中所要调用的委托对象方法也没有进行限制,所以可以更改委托对象和委托对象所要调用的方法。

   这是这里要重点强调一点就是:这里实现了InvocationHandler接口的UserHandler不是代理类,虽然它跟静态代理模式中的代理类结构很类似,UserHandler类只是定义了操作代码(这里的权限检查),顺道在执行操作代码之后调用委托对象的方法(听起来很像代理类),为了避免混淆,这里称UserHandler类为操作类,突出其执行操作代码的作用。动态代理中并不能按照静态代理结构理解,委托对象在哪个类中,哪个类就是代理类。

   到了这里还有一个问题,虽然实现了InvocationHandler接口的UserHandler的操作类实现了操作代码,并且可以完成泛型委托对象的方法调用,但是UserHandler操作类怎么去调用诸多接口声明的方法?UserHandler类并没有实现这些接口,所以虽然UserHandler类强大到可以调用任何类型委托对象的方法,但是这些方法对象(Method对象)怎么传进来?如果这时候有个实现了所有接口的类就好了,这样就可以把要调用的接口方法作为对象传递给UserHandler类,然后所有这些接口类型的委托对象都可以在执行完操作代码后调用这些方法。

   jdk中提供有一个类Proxy,Proxy类中的newProxyInstance方法体中可以产生实现了所有指定接口的类,这里不妨称之为$Proxy,此$Proxy类即为代理类,不过与静态代理模式稍有不同的是,该$Proxy类中并没有保存委托类对象,而只是将要调用的方法对象传递给操作类,然后调用操作类的invoke方法(操作类中只有这一个方法),然后的过程上面就已经讲过了,在invoke方法中执行操作代码(权限检查),然后利用委托对象来完成传递过来的方法对象的调用。

   所以仔细一想,动态代理其实是将委托对象和委托对象要执行的方法分开了,由实现了所有给定接口的$Proxy类来确定待执行方法对象,由实现了InvocationHandler接口的操作类来确定委托对象,然后在操作类的invoke方法中完成操作代码的执行和方法对象的调用。执行流程如下:


根据上面的User接口和委托类UserImp,使用UserHandler操作类来进行动态代理示例:

package test;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;

public class client2 {

	public static void main(String[] args) {
		User user=new UserImp();//委托类对象
		
		InvocationHandler h=new UserHandler(user);//构造方法注入
		User proxy=(User)Proxy.newProxyInstance(user.getClass().getClassLoader(), new Class[]{User.class},h);
		proxy.addUser();
	}

}

直接结果如下:

access control
User add

   Proxy的newProxyInstance静态方法分别传入类加载器,$Proxy代理类待实现的接口数组,以及给定了操作代码的操作类。

这里解释一下invoke方法的参数:

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable;

   对于第二个Method方法对象和第三个Object[]方法参数数组没什么好说的,加上第一个proxy对象,都是由$Proxy类执行InvocationHandler引用对象的invoke方法时传入的参数,这里执行Method方法的对象是委托类对象,而非传入的proxy对象,观察上面的执行代码:

User user=new UserImp();//委托类对象
InvocationHandler h=new UserHandler(user);//构造方法注入
User proxy=(User)Proxy.newProxyInstance(user.getClass().getClassLoader(), new Class[]{User.class},h);
proxy.addUser();

   这里进行$Proxy代理类对象proxy的addUser方法调用,在$Proxy类中的addUser方法内部执行的是InvocationHandler实现类对象handler的invoke方法,invoke方法传入的三个参数,其中第一个参数proxy就是最开始执行proxy.addUser方法的调用者本身,调用过程如下:

   观察上面的UserHandler类,method.invoke(obj,args);如果不是obj来调用方法,而是改成proxy来调用方法,则会陷入无限递归,直至超过虚拟机定义最深递归层数。那么将代理类对象proxy传入有什么作用,参考观察者模式中,当观察目标的状态发生更改,将影响传播到观察者,调用观察者的update(this)方法时,将自身对象的应用传递过去,可以方便观察者根据目标的状态进行自身的更新。所以这里调用方法并将自身作为参数传递到方法参数中,属于常用手段,以方便获取有关方法调用者的相关信息,例如这里可以在invoke方法执行中打印出代理类名称(虽然打印名称也没什么用),这里代理类名称为com.sun.proxy.$Proxy0。





评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值