从零开始探索jdk动态代理

前言

最近正在学习Spring,和以往一样,学习资料的来源依然是万能的3W。作为一个非专业IT人,以往的我并没有整理笔记的习惯,但最近的记忆力给我带来了些许困扰。于是本人的博客之旅就这样开始了…
作为第一篇博文,为了不打击自己的积极性,内容上主要参考了知乎大大bravo1988,若有侵权望告知。
言归正传,Spring的核心是控制反转(IOC)和面向切面(AOP),而AOP的底层实现便是本文的核心–动态代理。

1.代理模式

百度百科中代理模式是这样定义的:为其他对象提供一种代理以控制对这个对象的访问。在某些情况下,一个对象不适合或者不能直接引用另一个对象,而代理对象可以在客户端和目标对象之间起到中介的作用。
代理模式的UML图如下:
代理模式UML图
代理模式一般分为静态代理和动态代理,为了直观的描述静态代理和动态代理,下面以一个例子来说明。
假设我们现在有个类Student如下:

public class Student {
	
	public String getSex(int stu_id){
		String sex = stu_id==1?"Girl":"ToDo";
			return sex;
	}
	
	public int getAge(int stu_id){
		int age = stu_id==1?15:-1;
		return age;
	}
}

现在要完成如下需求:在对象的每个方法前后加上方法开始及结束说明。

1.1静态代理

静态代理指在程序运行前已经存在代理类的字节码文件,静态代理的原理可以用下图表示。
static_proxy_1
为了完成上述需求,计划定义如下三个类
1.Student接口Interface Student
2.Student接口实现类StudentImpl(真实对象类)
3.Student接口代理类StudentProxy(代理对象类)

public interface Student {
	public String getSex(int stu_id);
	
	public int getAge(int stu_id);
}
public class StudentImpl implements Student{
	
	public String getSex(int stu_id){
		String sex = stu_id==1?"Girl":"ToDo";
			return sex;
	}
	
	public int getAge(int stu_id){
		int age = stu_id==1?15:-1;
		return age;
	}
}

public class StudentProxy implements Student{
	public Student realSubject;
	
	public StudentProxy(Student realSubject){
		this.realSubject = realSubject;
	}
	
	public String getSex(int stu_id){
		System.out.println("getSex方法开始");
		String sex = realSubject.getSex(stu_id);
		System.out.println("getSex方法结束");
			return sex;
	}
	
	public int getAge(int stu_id){
		System.out.println("getAge方法开始");
		int age = realSubject.getAge(stu_id);
		System.out.println("getAge方法开始");
		return age;
	}
}

下面开始测试

public class StudentStaticProxyTest {
	public static void main(String args[]){
		Student student = new StudentProxy(new StudentImpl());
		int stu_id=1;
		String sex = student.getSex(stu_id);
		System.out.println(stu_id+"号学生性别"+sex);
		int age = student.getAge(stu_id);
		System.out.println(stu_id+"号学生年龄为"+age);
	}
}

测试结果如下
test_result
通过静态代理完成了需求,但从实现的方法看静态代理存在着如下缺陷:
1.每个方法都需要添加说明,若方法有很多个,代理类编码效率低
2.当需求改变时,代理类每个方法都要改变,不易修改维护
3.一个代理类只能实现一种接口的代理,如果有许多接口需要代理,工作量较大

通过上述分析,代理类只是用于生成代理对象,代理对象才是最终需求,那么能不能直接通过对象接口动态生成代理对象呢?
其实这些问题前辈们早已做了探索,JDK中便存在着这样的方法,接下来将详细介绍JDK动态代理。

1.2 JDK动态代理

同样还是以上文提及的需求为例开始介绍。在介绍静态代理的时候,为了实现Student类的代理,编写了StudentProxy类,这正是静态代理存在着的缺陷,需要为每一个代理对象编写代理类。那么能不能不通过定义代理类直接生成代理对象呢?
这里先回顾一下java对象是如何生成的。
在这里插入图片描述
由此可见对象是由class字节码生成的,这刚好解决了上述问题,可以不通过定义代理类直接由class加载到内存生成的字节码生成对象,找啊找,终于在jdk中找到了直接生成Class的方法。
在这里插入图片描述
但仔细查看,又遇到了新问题,Class的构造方法是Private类型,外部无法调用,看来只能找其他的方法了。
又开始了jdk探索之旅,看看JDK中有没有方法可以通过代理接口类得到class,不负众望,在Proxy类中果然存在着gteProxyClass这样一个静态方法。
在这里插入图片描述
和当初期望的一样,只需传入代理对象接口的类加载器,代理对象的接口无需代理类就可以返回一个代理对象。有了这个方法,下面可以测试了。
首先做一个准备工作,看一下真实对象的接口及实现类的Class

public class ClassTest {
	public static void main(String[] args) { 
	/*Student接口的Class对象 得到Class对象的三种方式:
	 * 1.Class.forName(xxx) 
	 * 2.xxx.class 
	 * 3.xxx.getClass() 
	 * 注意,这并不是我们new了一个Class对象,而是让虚拟机加载并创建Class对象
	 */ 
	 Class<Student> studentClass = Student.class; 
	 //Student接口的构造器信息 
	 Constructor[] studentClassConstructors = studentClass.getConstructors(); 
	 //Student接口的方法信息 
	 Method[] studentClassMethods = studentClass.getMethods(); 
	 //打印 
	 System.out.println("------接口Class的构造器信息------"); 
	 printClassInfo(studentClassConstructors); 
	 System.out.println("------接口Class的方法信息------"); 
	 printClassInfo(studentClassMethods); 
	 //Student实现类的Class对象
	 Class<StudentImpl> studentImplClass = StudentImpl.class; 
	 //Student实现类的构造器信息 
	 Constructor<?>[] studentImplClassConstructors = studentImplClass.getConstructors();
	 //Student实现类的方法信息 
	 Method[] studentImplClassMethods = studentImplClass.getMethods();
	 //打印
	 System.out.println("------实现类Class的构造器信息------");
	 printClassInfo(studentImplClassConstructors); 
	 System.out.println("------实现类Class的方法信息------"); 
	 printClassInfo(studentImplClassMethods); 
	 } 
public static void printClassInfo(Executable[] targets){ 
	for (Executable target : targets) { 
		// 构造器/方法名称 
		String name = target.getName(); 
		StringBuilder sBuilder = new StringBuilder(name); 
		// 拼接左括号 
		sBuilder.append('('); 
		Class[] clazzParams = target.getParameterTypes(); 
		// 拼接参数
		for(Class clazzParam : clazzParams){ 
			sBuilder.append(clazzParam.getName()).append(','); } 
		//删除最后一个参数的逗号
		if(clazzParams!=null && clazzParams.length != 0) 
		{ sBuilder.deleteCharAt(sBuilder.length()-1); } 
		//拼接右括号 
		sBuilder.append(')'); 
		//打印 构造器/方法 
		System.out.println(sBuilder.toString()); 
		} 
}

}

在这里插入图片描述
从上述结果可以看出接口Class没有构造方法,实现类Class除了有接口的方法还有从Object类继承的方法
接着用getProxyClass进行测试

public class StudentDynamicProxyTest {
	public static void main(String[] args) { 

		Class studentProxyClass = Proxy.getProxyClass(
				Student.class.getClassLoader(), Student.class); 
		//以Student实现类的Class对象作对比,看看代理Class是什么类型 
		System.out.println(StudentImpl.class.getName()); 
		System.out.println(studentProxyClass.getName()); 
		//打印代理Class对象的构造器 
		Constructor[] constructors = studentProxyClass.getConstructors(); 
		System.out.println("----构造器----"); 
		printClassInfo(constructors); 
		//打印代理Class对象的方法
		Method[] methods = studentProxyClass.getMethods(); 
		System.out.println("----方法----"); 
		printClassInfo(methods); } 
	public static void printClassInfo(Executable[] targets) { 
		for (Executable target : targets) { 
			// 构造器/方法名称 
			String name = target.getName(); 
			StringBuilder sBuilder = new StringBuilder(name); 
			// 拼接左括号 
			sBuilder.append('('); 
			Class[] clazzParams = target.getParameterTypes();
			// 拼接参数 
			for (Class clazzParam : clazzParams) {
				sBuilder.append(clazzParam.getName()).append(','); } 
			//删除最后一个参数的逗号
			if (clazzParams != null && clazzParams.length != 0) 
			{ sBuilder.deleteCharAt(sBuilder.length() - 1); } 
			//拼接右括号 
			sBuilder.append(')'); 
			//打印 构造器/方法 
			System.out.println(sBuilder.toString()); 
			} 
		} 
}

在这里插入图片描述
从上述结果可以看出通过给Proxy.getProxyClass()传入对象接口加载器和接口类,得到了一个强化版的Class:即包含接口的方法信息getAge()、getSex(),又包含了构造器$Proxy0(InvocationHandler),还有一些自己特有的方法以及从Object继承的方法。
既然这个Class既有方法信息,又有构造器,下一步通过它生成一个代理对象

public class StudentDynamicProxyTest {
	public static void main(String[] args) throws Throwable{ 
		
		Class studentProxyClass = Proxy.getProxyClass(
				Student.class.getClassLoader(), Student.class);
		Constructor constructor = studentProxyClass.getConstructor(InvocationHandler.class);
		Student studentProxyImpl = (Student)constructor.newInstance(new InvocationHandler(){
			@Override
			public Object invoke(Object proxy ,Method method,Object[] args){
				
				return null;
			}
		});
		System.out.println(studentProxyImpl.getAge(1));
		System.out.println(studentProxyImpl.getSex(1));
		} 
}

在这里插入图片描述
居然报了空指针错误,联想到前面,只有在nvocationHandler的invoke()返回null,难道是因为这个?带着疑问,编写如下测试类

public class StudentDynamicProxyTest {
	public static void main(String[] args) throws Throwable{ 
		
		Class studentProxyClass = Proxy.getProxyClass(
				Student.class.getClassLoader(), Student.class);
		Constructor constructor = studentProxyClass.getConstructor(InvocationHandler.class);
		Student studentProxyImpl = (Student)constructor.newInstance(new InvocationHandler(){
			@Override
			public Object invoke(Object proxy ,Method method,Object[] args){
				
				return 1;
			}
		});
		System.out.println(studentProxyImpl.getAge(1));
		System.out.println(studentProxyImpl.getSex(1));
		} 
}

在这里插入图片描述
将返回值改为1后,getAge方法不报错,getSex方法报错,并报出Integer无法转为String的错误,据此推测每次调用代理对象的方法都会调用invoke,而且invoke的返回值就是代理方法的返回值,正是因为getAge返回值为整型,而invoke返回值被设为1所以此方法不报错,而返回值为String的getSex方法报错。
通过推理动态代理的Class大致如下
在这里插入图片描述
这样设计有一个明显的有点,方法与方法体分离耦合性降低。
但是最终的需求是调用真实对象的方法,那么如何调用呢?注意到invoke函数的后面两个参数,方法及方法参数,由于代理对象和真实对象继承于同一个接口,方法是同名的,可以通过方法的反射调用执行真实对象,具体测试如下。

public class Test3 {
	public static void main(String[] args) throws Throwable {

		StudentImpl target = new StudentImpl(); 
		//传入目标对象 
		//目的:1.根据它实现的接口生成代理对象 2.代理对象调用目标对象方法
		Student studentProxy = (Student) getProxy(target);
		studentProxy.getAge(1); 
		studentProxy.getSex(1); 
		} 
	private static Object getProxy(final Object target) throws Exception { 
		//参数1:随便找个类加载器给它, 参数2:目标对象实现的接口,让代理对象实现相同接口 
		Class proxyClass = Proxy.getProxyClass(
				target.getClass().getClassLoader(), target.getClass().getInterfaces()); 
		Constructor constructor = proxyClass.getConstructor(InvocationHandler.class); 
		Object proxy = constructor.newInstance(new InvocationHandler() { 
			@Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { 
				System.out.println(method.getName() + "方法开始"); 
				Object result = method.invoke(target, args); 
				System.out.println(result); System.out.println(method.getName() + "方法结束"); 
				return result; } });
		return proxy; } 
}

在这里插入图片描述
到这jdk动态代理的原理基本了解了,当然前辈们早已准备了更简单的方法
在这里插入图片描述
此时是不是顿时感觉神清气爽,但是方法的提示依旧在invoke函数体中,耦合性大,不利于后期扩展,那么有没有好的办法能将其抽离呢?答案是肯定的
定义一个Log接口及实现类

public interface Log {
	
	public void beforeMethod(Method method);
	
	public void afterMethod(Method method);
	
}
public class LogImpl implements Log{
	public void beforeMethod(Method method){
		System.out.println(method.getName() + "方法开始"); 
	};
	
	public void afterMethod(Method method){
		System.out.println(method.getName() + "方法结束"); 
	};
}

测试类如下

public class Test4 {
	public static void main(String[] args) throws Throwable {

		StudentImpl target = new StudentImpl(); 
		//传入目标对象 
		//目的:1.根据它实现的接口生成代理对象 2.代理对象调用目标对象方法
		Student studentProxy = (Student) getProxy(target,new LogImpl());
		studentProxy.getAge(1); 
		studentProxy.getSex(1); 
		} 
	private static Object getProxy(final Object target ,Log log) throws Exception { 
		/*代理对象的方法最终都会被JVM导向它的invoke方法*/
		Object proxy = Proxy.newProxyInstance(
				target.getClass().getClassLoader(),/*类加载器*/
				target.getClass().getInterfaces(),/*让代理对象和目标对象实现相同接口*/
				(proxy1, method, args) -> {
					log.beforeMethod(method); 
					Object result = method.invoke(target, args); 
					System.out.println(result); 
					log.afterMethod(method); 
					return result; 
				}
				);
		return proxy; 
	}
}

在这里插入图片描述
其实Spring的AOP实现原理大概也就是这样了,只是细节更复杂一点。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值