浅谈JDK动态代理

本文详细讲解了如何使用静态代理和动态代理实现给Calculator类添加日志打印,探讨了静态代理的局限性和动态代理的自动代理生成机制。通过JDK动态代理,实现了通知(日志)的分离,遵循开闭原则,提高了代码的灵活性和维护性。

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

前言:尽管动态代理看起来似乎有一定难度,但却必须拿下。因为Spring的事务控制依赖于AOP,AOP底层实现便是动态代理,环环相扣。到最后,还是看基本功。


主要内容:

  • 一个小需求:给原有方法添加日志打印
  • 静态代理实现日志打印
  • 静态代理的问题

一个小需求:给原有方法添加日志打印

假设现在我们有一个类Calculator,代表一个计算器,它可以进行加减乘除操作。

public class Calculator {

	//加
	public int add(int a, int b) {
		int result = a + b;
		return result;
	}

	//减
	public int subtract(int a, int b) {
		int result = a - b;
		return result;
	}

	//乘法、除法...
}

现有一个需求:在每个方法执行前后打印日志。


直接修改

最直观的想法是直接修改Calculator类:

public class Calculator {

	//加
	public int add(int a, int b) {
		System.out.println("add方法开始...");
		int result = a + b;
		System.out.println("add方法结束...");
		return result;
	}

	//减
	public int subtract(int a, int b) {
		System.out.println("subtract方法开始...");
		int result = a - b;
		System.out.println("subtract方法结束...");
		return result;
	}

	//乘法、除法...
}

上面的方案是有问题的:

  1. 直接修改源程序,不符合开闭原则。应该对扩展开放,对修改关闭
  2. 如果Calculator有几十个、上百个方法,修改量太大
  3. 存在重复代码(都是在核心代码前后打印日志)
  4. 日志打印硬编码在代理类中,不利于后期维护:比如你花了一上午终于写完了,组长告诉你这个功能取消,于是你又要打开Calculator花十分钟删除日志打印的代码!

所以,此种方案PASS!



静态代理实现日志打印

“静态代理”四个字包含了两个概念:静态、代理。我们先来了解什么叫“代理”,至于何为“静态”,需要和“动态”对比着讲。

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

在这里插入图片描述

常用的代理方式可以粗分为:静态代理和动态代理。

静态代理的实现比较简单:编写一个代理类,实现与目标对象相同的接口,并在内部维护一个目标对象的引用。通过构造器塞入目标对象,在代理对象中调用目标对象的同名方法,并添加前拦截,后拦截等所需的业务功能。

按上面的描述,代理类和目标类需要实现同一个接口,所以我打算这样做:

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

接口:

/**
 * Calculator接口
 */
public interface Calculator {
	int add(int a, int b);
	int subtract(int a, int b);
}

目标对象实现类:

/**
 * 目标对象实现类,实现Calculator接口
 */
public class CalculatorImpl implements Calculator {

	//加
	public int add(int a, int b) {
		int result = a + b;
		return result;
	}

	//减
	public int subtract(int a, int b) {
		int result = a - b;
		return result;
	}

	//乘法、除法...
}

代理对象实现类:

/**
 * 代理对象实现类,实现Calculator接口
 */
public class CalculatorProxy implements Calculator {
        //代理对象内部维护一个目标对象引用
	private Calculator target;
        
        //构造方法,传入目标对象
	public CalculatorProxy(Calculator target) {
		this.target = target;
	}

        //调用目标对象的add,并在前后打印日志
	@Override
	public int add(int a, int b) {
		System.out.println("add方法开始...");
		int result = target.add(a, b);
		System.out.println("add方法结束...");
		return result;
	}

        //调用目标对象的subtract,并在前后打印日志
	@Override
	public int subtract(int a, int b) {
		System.out.println("subtract方法开始...");
		int result = target.subtract(a, b);
		System.out.println("subtract方法结束...");
		return result;
	}

	//乘法、除法...
}

使用代理对象完成加减乘除,并且打印日志:

public class Test {
	public static void main(String[] args) {
		//把目标对象通过构造器塞入代理对象
		Calculator calculator = new CalculatorProxy(new CalculatorImpl());
		//代理对象调用目标对象方法完成计算,并在前后打印日志
		calculator.add(1, 2);
		calculator.subtract(2, 1);
	}
}

静态代理的优点:可以在不修改目标对象的前提下,对目标对象进行功能的扩展和拦截。但是它也仅仅解决了上一种方案4大缺点中的第1点:

  • 直接修改源程序,不符合开闭原则。应该对扩展开放,对修改关闭 √
  • 如果Calculator有几十个、上百个方法,修改量太大 ×
  • 存在重复代码(都是在核心代码前后打印日志) ×
  • 日志打印硬编码在代理类中,不利于后期维护:比如你花了一上午终于写完了,组长告诉你这个功能取消,于是你又要打开Calculator花十分钟删除全部新增代码!×


静态代理的问题

上面案例中,代理类是我们事先编写的,而且要和目标对象类实现相同接口。由于CalculatorImpl(目标对象)需要日志功能,我们即编写了CalculatorProxy(代理对象),并通过构造器传入CalculatorImpl(目标对象),调用目标对象同名方法的同时添加增强代码。

但是这里有个问题!代理对象构造器的参数类型是Calculator,这意味着它只能接受Calculator的实现类对象,亦即我们写的代理类CalculatorProxy只能给Calculator做代理,它们绑定死了!

在这里插入图片描述

如果现在我们系统需要全面改造,给其他类也添加日志打印功能,就得为其他几百个接口都各自写一份代理类:

在这里插入图片描述

自己手动写一个类并实现接口实在太麻烦了。仔细一想,我们其实想要的并不是代理类,而是代理对象!那么,能否让JVM根据接口自动生成代理对象呢?比如,有没有一个方法,我传入接口,它就给我自动返回代理对象呢?

答案是肯定的。


我们来看看JDK如何自动生成代理对象!



在使用动态代理之前我们先进行接口创建对象的可行性分析。

虽然知道源代码经过javac命令编译后会在磁盘中得到字节码文件(.class文件),也知道java命令会启动JVM将字节码文件加载进内存,但也仅仅止步于此了。至于从字节码文件加载进内存到堆中产生对象,期间具体发生了什么,他们并不清楚。

所谓“万物皆对象”,字节码文件也难逃“被对象”的命运。它被加载进内存后,JVM为其创建了一个对象,以后所有该类的实例,皆以它为模板。这个对象叫Class对象,它是Class类的实例。

在这里插入图片描述

也就是说,要得到一个类的实例,关键是先得到该类的Class对象!只不过new这个关键字实在太方便,为我们隐藏了底层很多细节,我在刚开始学习Java时甚至没意识到Class对象的存在。

  • 接口Class对象没有构造方法,所以Calculator接口不能直接new对象
  • 实现类Class对象有构造方法,所以CalculatorImpl实现类可以new对象
  • 接口Class对象有两个方法add()、subtract()
  • 实现类Class对象除了add()、subtract(),还有从Object继承的方法

也就是说,接口和实现类的Class信息除了构造器,基本相似。

既然我们希望通过接口创建实例,就无法避开下面两个问题:

  1. 接口方法体缺失问题
    首先,接口的Class对象已经得到,它描述了方法信息。但它没方法体。没关系,反正代理对象的方法是个空壳,只要调用目标对象的方法即可。JVM可以在创建代理对象时,随便糊弄一个空的方法体,反正后期我们会想办法把目标对象塞进去调用。所以这个问题,勉强算是解决。
  2. 接口Class没有构造器
    无法new这个问题好像无解…毕竟这么多年了,的确没听哪位仁兄直接new接口的。但是,仔细想想,接口之所以不能new,是因为它缺少构造器,它本身是具备完善的类结构信息的。那动态代理是怎么创建实例的呢?


动态代理


不错,动态代理确实存在getXxxClass()这样的方法。

我们需要java.lang.reflect.InvocationHandler接口java.lang.reflect.Proxy类的支持。Proxy后面会用到InvocationHandler,因此我打算以Proxy为切入点。首先,再次明确我们的思路:

在这里插入图片描述

通过查看API,我们发现Proxy类有一个静态方法可以帮助我们。

在这里插入图片描述

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

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

在这里插入图片描述

也就是说,通过给Proxy.getProxyClass()传入类加载器和接口Class对象,我们得到了一个加强版的Class:既包含接口的方法信息add()、subtract(),又包含了构造器$Proxy0(InvocationHandler),还有一些自己特有的方法以及从Object继承的方法。


梳理一下:

  1. 原先我们本打算直接根据接口Class得到代理对象,无奈接口Class只有方法信息,没有构造器
  2. 于是,我们想,有没有办法创建一个Class对象,既有接口Class的方法信息,同时又包含构造器方便创建代理实例呢?
  3. 利用Proxy类的静态方法getProxyClass()方法,给它传一个接口Class对象,它能返回一个加强版Class对象。也就是说getProxyClass()的本质是:用Class,造Class。

在这里插入图片描述

要谢谢Proxy类和JVM,让我们不写代理类却直接得到代理Class对象,进而得到代理对象。

在这里插入图片描述

既然Class<$Proxy0>有方法信息,又有构造器,我们试着用它得到代理实例吧:

我们发现,newInstance()创建对象失败。因为Class的newInstance()方法底层会走无参构造器。而之前打印$Proxy0的Class信息时,我们发现它没有无参构造,只有有参构造$Proxy0(InvocationHandler)。那就靠它了:

在这里插入图片描述

竟然发生了空指针异常。纵观整个代码,新写的add()和subtract()返回值是int,不会是空指针。而再往上的代码之前编译都是通过的,应该没问题啊。再三思量,我们发现匿名对象InvocationHandler的invoke()返回null。难道是它?做个实验:让invoke()返回1,然后观察结果。

在这里插入图片描述

就目前的实验来看,调用过程应该是这样:

在这里插入图片描述


动态代理底层调用逻辑:

动态代理:constructor反射创建代理对象时,需要传入InvocationHandler,我猜,代理对象内部有一个成员变量InvocationHandler:

在这里插入图片描述

动态代理的大致设计思路就是:

在这里插入图片描述

在这里插入图片描述

但是我们真正想要的结果是:调用代理对象的方法时,去调用目标对象的方法。

所以,接下来努力的方向就是:设法在invoke()方法得到目标对象,并调用目标对象的同名方法。


代理对象调用目标对象方法

在这里插入图片描述

使用动态代理,让我们避免手写代理类,只要给getProxy()方法传入target就可以生成对应的代理对象。但是日志打印仍是硬编码在invoke()方法中。虽然修改时只要改一处,但是别忘了“开闭原则”。所以最好是能把日志打印单独拆出来,像目标对象一样作为参数传入。

日志打印其实就是AOP里的通知概念。我打算定义一个Advice接口,并且写一个MyLogger实现该接口。


通知接口

public interface Advice {
	void beforeMethod(Method method);
	void afterMethod(Method method);
}

日志打印

public class MyLogger implements Advice {

	public void beforeMethod(Method method) {
		System.out.println(method.getName() + "方法执行开始...");
	}

	public void afterMethod(Method method) {
		System.out.println(method.getName() + "方法执行结束...");
	}
}

测试类

public class ProxyTest {
	public static void main(String[] args) throws Throwable {
		CalculatorImpl target = new CalculatorImpl();
		Calculator calculatorProxy = (Calculator) getProxy(target, new MyLogger());
		calculatorProxy.add(1, 2);
		calculatorProxy.subtract(2, 1);
	}

	private static Object getProxy(final Object target, Advice logger) throws Exception {
		/*代理对象的方法最终都会被JVM导向它的invoke方法*/
		Object proxy = Proxy.newProxyInstance(
				target.getClass().getClassLoader(),/*类加载器*/
				target.getClass().getInterfaces(),/*让代理对象和目标对象实现相同接口*/
				(proxy1, method, args) -> {
					logger.beforeMethod(method);
					Object result = method.invoke(target, args);
					System.out.println(result);
					logger.afterMethod(method);
					return result;
				}
		);
		return proxy;
	}
}


小结


静态代理

代理类CalculatorProxy是我们事先写好的,编译后得到Proxy.class字节码文件。随后和目标类一起被ClassLoader(类加载器)加载进内存,生成Class对象,最后生成实例对象。代理对象中有目标对象的引用,调用同名方法并前后加上日志打印。

  • 优点:不用修改目标类源码
  • 缺点:高度绑定,不通用。硬编码,不易于维护。

动态代理

我们本想通过接口Class直接创建代理实例,无奈的是,接口Class虽然有方法信息描述,却没有构造器,无法创建对象。所以我们希望JDK能提供一套API,我们传入接口Class,它自动复制里面的方法信息,造出一个有构造器、能创建实例的代理Class对象。

在这里插入图片描述

  • 优点:
    不用写代理类,根据目标对象直接生成代理对象
    通知可以传入,不是硬编码
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值