JDK动态代理

一、学习Spring的AOP前掌握动态代理

因为Spring的事务控制依赖于AOP,AOP底层实现便是动态代理 + 责任链,环环相扣。

二、拟定需求:给原有方法添加日志打印

直接修改源码的缺点:
	直接修改源程序,不符合开闭原则,即好的程序设计应该对扩展开放,对修改关闭;
	如果“目标类”内部有几十个、上百个方法,修改量太大;
	存在重复代码(都是在核心代码前后打印日志);
	日志打印硬编码在代理类中,不利于后期维护:比如你花了一上午终于写完了,组长告诉你这个功能不做了,于是你又要打开修改“目标类”花十分钟删除日志打印的代码(或回滚分支)!

三、静态代理

代理:
代理是一种模式,提供了对目标对象的间接访问方式,即通过代理访问目标对象。如此便于在目标实现的基础上增加额外的功能操作,前拦截,后拦截等,以满足自身的业务需求。
image

静态代理实现方式:
编写一个代理类,实现与目标对象相同的接口,并在内部维护一个目标对象的引用。通过构造器塞入目标对象,在代理对象中调用目标对象的同名方法,并添加前拦截,后拦截等所需的业务功能。
image
更好的实现应该是:(按上面的描述,代理类和目标类需要实现同一个接口)

  • 将Calculator抽取为接口
  • 创建目标类CalculatorImpl实现Calculator
  • 创建代理类CalculatorProxy实现Calculator

静态代理完成需求度:

静态代理的优点:可以在不修改目标对象的前提下,对目标对象进行功能的扩展和拦截。但是它也仅仅解决了“直接修改源码”方案4大缺点中的第1、4两点:
	直接修改源程序,不符合开闭原则,即好的程序设计应该对扩展开放,对修改关闭(✅,如果一开始就面向接口编程,这一步其实是不需要的)
	如果“目标类”内部有几十个、上百个方法,修改量太大(❎,目标类有多少个方法,代理类就要重写多少个方法)
	存在重复代码(都是在核心代码前后打印日志)(❎,代理类中的日志代码是重复的)
	日志打印硬编码在代理类中,不利于后期维护:比如你花了一上午终于写完了,组长告诉你这个功能不做了(✅,别用代理类就好了)

我们会发现:
我们其实想要的并不是代理类,而是代理对象!!!

四、动态代理

动态代理如果要优于静态代理,至少需要解决两个问题:

  • 自动生成代理对象,让程序员免受编写代理类的痛苦
  • 将增强代码与代理类(代理对象)解耦,从而达到代码复用

image

1、实现自动生成代理对象

实现自动生成代理对象依靠的是反射机制,我们学习对象的创建过程会知道:
image
代理类和实例对象之间其实还隔着一个Class对象。如果能得到Class对象,就能生成实例。所以,现在的问题又变成:如何不写代理类,直接得到Class对象

Class类(Class类就是用来描述“类”的):所谓“万物皆对象”,字节码文件也难逃“被对象”的命运。当字节码文件(.class文件)被加载进内存后,JVM也为其创建了一个对象,以后所有该类的实例,皆以它为模板。这个对象叫Class对象,它是Class类的实例。
image
Person类有两个不同维度的对象:
* 根据Person类实例化得到的Person p1、 p2、p3对象
* Class类实例化得到的Class personClass对象

补充:
得到Class对象的方法:
	Class.forName(xxx):Class<Person> clazz = Class.forName("com.bravo.Person");
	xxx.class:Class<Person> clazz = Person.class;
	xxx.getClass():Class<Person> clazz = person.getClass();

但是,这三种方式都需要先有类,但我们不想编写代理类!

这里好像断了思路;
但是我们如果仔细思考会发现,代理类或者代理对象其实只是起到中间更改的作用,而最重要的是最后要实现的结果,也就是说最重要的其实是 增强代码 + 目标对象。我们对代理对象的要求很低,只需要与目标对象拥有相同的方法即可。

所以本质上,代理对象只要有方法申明即可,甚至不需要方法体,或者只要一个空的方法体即可,反正我们会把目标对象返回去。

那么,如何知道一个类有哪些方法信息呢?如果能得到类的方法信息,我们或许可以直接造一个代理对象。

有两个途径:

  • 目标类本身
  • 目标类实现的接口

这两个思路造就了两种不同的代理机制,一个被后人称为CGLib动态代理,另一个则被JDK收录,世人称之为JDK动态代理。本文重点介绍JDK动态代理。

但是接口Class对象没有构造方法,所以接口不能直接new对象;(接口缺少构造器信息)

那怎么根据一个接口得到代理对象?

JDK选择了 不改变接口本身,直接拷贝接口的信息到另一个Class,然后给那个Class装上构造器呢。
也就是:拷贝接口Class的信息,产生一个新的Class对象。

也就是说,JDK动态代理的本质是:用Class造Class,即用接口Class造出一个代理类Class。

image

实现了这个方法的API是:

  • Proxy.getProxyClass():返回代理类的Class对象。
    image

也就说,只要传入接口的Class对象,getProxyClass()方法即可返回代理Class对象,而不用实际编写代理类。这相当于什么概念?
image

而Proxy.getProxyClass()返回的Class对象是有构造器的!

image

开头说了,动态代理的使命有两个:

  • 自动生成代理对象,让程序员免受编写代理类的痛苦
  • 将增强代码与代理类(代理对象)解耦,从而达到代码复用

现在我们已经得到了代理Class,只需通过反射即可得到代理对象。

Proxy.getProxyClass()使用流程:
  • 先获得proxyClazz
  • 再根据proxyClazz.getConstructor()获取构造器
  • 最后constructor.newInstance()生成代理对象

JDK已经提供了一步到位的方法Proxy.newProxyInstance(),来帮我们封装上面的步骤。

实例代码对比:
使用InvocationHandler对象的的目的就是为了实现 将增强代码与代理类(代理对象)解耦,从而达到代码复用

未使用Proxy.newProxyInstance():

public class ProxyTest {
	public static void main(String[] args) throws Throwable {
		// 1.得到目标对象
		CalculatorImpl target = new CalculatorImpl();
		// 2.传入目标对象,得到增强对象(如果需要对目标对象进行别的增强,可以另外编写getXxInvocationHandler)
		InvocationHandler logInvocationHandler = getLogInvocationHandler(target);
		// 3.传入接口+增强对象(含目标对象),得到代理对象
		Calculator calculatorProxy = (Calculator) getProxy(
				logInvocationHandler,                 // 增强对象(包含 目标对象 + 增强代码)
				target.getClass().getClassLoader(),   // 随便传入一个类加载器
				target.getClass().getInterfaces()     // 需要代理的接口
		);
		calculatorProxy.add(1, 2);
	}

	/**
	 * 传入接口+增强(已经包含了目标对象),获取代理对象
	 *
	 * @param handler
	 * @param classLoader
	 * @param interfaces
	 * @return
	 * @throws Exception
	 */
	private static Object getProxy(final InvocationHandler handler, final ClassLoader classLoader, final Class<?>... interfaces) throws Exception {
		// 参数1:随便找个类加载器给它 参数2:需要代理的接口
		Class<?> proxyClazz = Proxy.getProxyClass(classLoader, interfaces);
		Constructor<?> constructor = proxyClazz.getConstructor(InvocationHandler.class);
		return constructor.newInstance(handler);
	}

	/**
	 * 日志增强代码
	 *
	 * @param target
	 * @return
	 */
	private static InvocationHandler getLogInvocationHandler(final CalculatorImpl target) {
		return new InvocationHandler() {
			@Override
			public Object invoke(Object proxy1, 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;
			}
		};
	}
}

使用了Proxy.newProxyInstance():

public class ProxyTest {
	public static void main(String[] args) throws Throwable {
		// 1.得到目标对象
		CalculatorImpl target = new CalculatorImpl();
		// 2.传入目标对象,得到增强对象(如果需要对目标对象进行别的增强,可以另外编写getXxInvocationHandler)
		InvocationHandler logInvocationHandler = getLogInvocationHandler(target);
		// 3.传入目标对象+增强代码,得到代理对象(直接用JDK的方法!!!)
		Calculator calculatorProxy = (Calculator) Proxy.newProxyInstance(
				target.getClass().getClassLoader(),   // 随便传入一个类加载器
				target.getClass().getInterfaces(),    // 需要代理的接口
				logInvocationHandler                  // 增强对象(包含 目标对象 + 增强代码)

		);
		calculatorProxy.add(1, 2);
	}

	/**
	 * 日志增强代码
	 *
	 * @param target
	 * @return
	 */
	private static InvocationHandler getLogInvocationHandler(final CalculatorImpl target) {
		return new InvocationHandler() {
			@Override
			public Object invoke(Object proxy1, 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;
			}
		};
	}
}

学习自:
知乎-bravo1988

posted @ 2021-09-14 18:40  光一  阅读(288)  评论(0编辑  收藏  举报