动态代理

主要内容:

1.静态代理回顾
2.接口创建对象的可行性分析
3.动态代理
4.InvocationHandler接口
5.小结

该博文非原创,大部分抄录自zhihu用户:bravo1988的原创文章。主要用于本人学习梳理

1.静态代理回顾

  • 假设有一个需求,在项目现有所有类的方法前后打印日志。你如何在不修改已有代码的前提下,完成这个需求?

  • 静态代理实现:

    为现有的每一个类都编写一个对应的代理类,并且让代理类和目标类实现相同的接口。

    代理对象内部维系着一个目标对象的引用属性,通过构造器传入一个目标对象实例,来初始化这个引用属性。然后在代理对象的方法内部调用目标对象的同名的这个方法,并在调用前后打印日志。也就是说,代理对象 = 增强代码 + 目标对象(原对象),有了代理对象后,就不用原对象了。
    在这里插入图片描述

  • 静态代理的缺陷:

    每个代理类只能代理一个目标对象,所以要为每一个目标类或者每一个接口,都编写对应的代理类。如果当前系统有成千上万个类,那么工作量就太大了。所以,我们现在的努力方向就是:有没有抽取出来一个方法,传入什么对象,就能得到这个对象的代理对象。

2.接口创建实例对象的可行性分析

  • 2.1.回想一下对象的创建过程

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

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

在这里插入图片描述

  • 2.2.注意到在创建一个实例对象时,JVM会把.class文件加载进内存,然后创建一个Class类对象。

    Class类是描述所有类信息的,而这个Class类对象,是描述某个特定对象(你想要实例的对象)的。该类的具体结构信息都会被映射到Class类对象中,包括方法,构造器。所以我们可以通过该类的Class类对象,来得到实例对象。

    也就是说,要得到一个类的实例,关键是先得到该类的Class对象!只不过new这个关键字实在太方便了,为我们隐藏了很多底层细节。

  • 2.3.那么怎么得到某该类的Class对象呢?

    Class类的构造器是private的,杜绝了外界通过new创建Class对象的可能。当程序需要某个类时,JVM自己会调用这个构造器,并传入ClassLoader(类加载器),让它去加载字节码文件到内存,然后JVM为其创建对应的Class对象。

    所以我看换种方式看看对象的创建过程;
    在这里插入图片描述
    所以关键一点:要得到一个类的实例,关键是要先得到该类的Class对象!

  • 2.4.看一下接口的Class类对象和类的Class类对象的区别:

    1.来分析一下接口Class和类Class的区别。以Calculator接口的Class对象和CalculatorImpl实现类的Class对象为例:

import java.lang.reflect.Constructor;
import java.lang.reflect.Executable;
import java.lang.reflect.Method;

public class ProxyTest {
	public static void main(String[] args) {
		/*Calculator接口的Class对象
                  得到Class对象的三种方式:1.Class.forName(xxx) 
                                           2.xxx.class 
                                           3.xxx.getClass()
                  注意,这并不是我们new了一个Class对象,而是让虚拟机加载并创建Class对象            
                */
		Class<Calculator> calculatorClazz = Calculator.class;
		//Calculator接口的构造器信息
		Constructor[] calculatorClazzConstructors = calculatorClazz.getConstructors();
		//Calculator接口的方法信息
		Method[] calculatorClazzMethods = calculatorClazz.getMethods();
		//打印
		System.out.println("------接口Class的构造器信息------");
		printClassInfo(calculatorClazzConstructors);
		System.out.println("------接口Class的方法信息------");
		printClassInfo(calculatorClazzMethods);

		//Calculator实现类的Class对象
		Class<CalculatorImpl> calculatorImplClazz = CalculatorImpl.class;
		//Calculator实现类的构造器信息
		Constructor<?>[] calculatorImplClazzConstructors = calculatorImplClazz.getConstructors();
		//Calculator实现类的方法信息
		Method[] calculatorImplClazzMethods = calculatorImplClazz.getMethods();
		//打印
		System.out.println("------实现类Class的构造器信息------");
		printClassInfo(calculatorImplClazzConstructors);
		System.out.println("------实现类Class的方法信息------");
		printClassInfo(calculatorImplClazzMethods);
	}

	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());
		}
	}
}

2.运行结果:
在这里插入图片描述
3.区别

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

那么两者的Class对象的主要区别就是:接口没有构造器。其他的结构都类似。也就是说,接口和对应的实现类的Class对象(信息)除了构造器,基本相似。

4.无论怎么说,我们都无法通过接口直接实例出一个对象。

即使考虑到接口的Class对象,还是没有构造器,所以无法通过接口直接实例出一个对象。

3.动态代理

回想一下我们想要什么?我们想要只通过一个接口,就能获得一个代理对象,来帮助我们增强某个类。但是上面分析的结果是,接口的Class对象仍然没有构造器,所以我们无法通过接口直接实例出一个对象。

在这里插入图片描述

那动态代理是怎么创建实例的呢?他有没有类似通过一个接口得到一个代理对象实例的方法呢?

下面我们需要java.lang.reflect.invocationHandler接口和java.lang.reflect.Proxy类的支持。

  • 3.1.通过查看API,我们发现Proxy类有一个静态方法可以帮助我们。
    在这里插入图片描述
    Proxy.getProxyClass():返回代理类的Class对象,参数是类加载器和一个接口的Class对象数组,得到一个代理类的Class类对线。

    也就是说,只要传入目标类实现的接口的Class对象,getProxyClass()方法就可以返回代理对象的Class类对象,而不用实际编写代理类。非常强大!
    在这里插入图片描述
    之后我们自然而然就可以用这个代理类的Class对象来实例创建一个代理对象。

  • 3.2.注意一下和得到静态代理对象的区别:

  • 代理对象要和目标对象有相似的结构,包括各种属性和方法等,所以每个类的对应的静态代理对象都会实现和这个类相同的接口。

	public class CalculatorImpl implements Calculator {
	}
  • 如果是静态代理的话,为什么每个类都要创建一个代理对象,就是因为每个类实现的接口情况是不同的,这个我们无法作为公共部分抽取出来。

  • 如果我们想要抽取出来一个方法,这个方法能够为每个类都创建一个代理对象,那么这个方法就要解决每个目标类都有不同的接口实现这样情况。所以抽取的方式,就是将这个实现的接口作为参数来满足上述要求(这个不太容易抽取出来的不同的部分,那么就抽取出来作为一个参数,因为每次传的参数可以是不同的)

    public static Class<?> getProxyClass(ClassLoader loader,
                                         Class<?>... interfaces)
  • 3.3.Proxy.getProxyClass(ClassLoader loader, Class<?>… interfaces)

    那么这个方法正好满足了我们上面分析的要求,将每个目标类实现的接口作为参数传递进来和一个类加载器,然后就能得到一个代理类的Class对象。

    我们看一下这个代理类Class对象中都有什么信息:

import java.lang.reflect.Constructor;
import java.lang.reflect.Executable;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

public class ProxyTest {
	public static void main(String[] args) {
		/*
		 * 参数1:Calculator的类加载器(当初把Calculator加载进内存的类加载器)
		 * 参数2:代理对象需要和目标对象实现相同接口Calculator
		 * */
		Class calculatorProxyClazz = Proxy.getProxyClass(Calculator.class.getClassLoader(), Calculator.class);
		//以Calculator实现类的Class对象作对比,看看代理Class是什么类型
        System.out.println(CalculatorImpl.class.getName());
		System.out.println(calculatorProxyClazz.getName());
		//打印代理Class对象的构造器
		Constructor[] constructors = calculatorProxyClazz.getConstructors();
		System.out.println("----构造器----");
		printClassInfo(constructors);
		//打印代理Class对象的方法
		Method[] methods = calculatorProxyClazz.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对象,我们得到了一个“加强版”的Class对象:即包含原接口的add(),subtract(),还包含一个构造器$Proxy0(InvocationHandler),还有一些自己特有的方法以及从Object继承的方法。

  • 3.4.梳理一下
    1.Class类中的newInstance()方法底层走的是类型参数T的无参构造
    2.原先我们打算直接根据接口的Class对象得到代理对象,但最后发现接口的Class只有方法信息,没有构造器。
    在这里插入图片描述

    3.于是我们想,有没有一个办法,能创建出一个Class对象,既有接口Class的方法信息,同时又包含构造器方便创建代理实例呢?
    4.最后找到Proxy类的静态方法getProxyClass()方法,给它传一个接口Class对象,就能返回一个代理类的Class对象,而且还有一个构造器。也就是说getProxyClass()的本质是:用(接口)Class对象,造(有参)的Class对象。

  • 3.5.小结一下
    getProxyClass()这个方法,会从你传入的接口的Class类对象数组中,“拷贝”类的结构信息放到一个新的Class对象,并返回。即根据传入的接口,得到代理类的Class对象。并且这个新的Class对象带有构造器,是可以创建对象的。这个构造器的参数是InvocationHandler接口的实现类。

    代理对象不需要实现接口,但是目标对象一定要实现接口,否则不能用动态代理。

  • 3.6.创建一下代理实例
    1.得到代理类的Class对象
    2.这个代理类的Class对象中维系着一个有参构造对象,拿到这个有参构造对象
    3.用这个有参构造对象实例出一个代理对象。

    /**
     * @param target
     *  传目标对象,方便调原方法
     * @return
     */
    public static Object generateProxy(Object target) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        //1.得到代理类Proxy的Class类对象:有InvocationHandle属性。Proxy类中有这个属性
        Class<?> proxyClass = Proxy.getProxyClass(target.getClass().getClassLoader(), target.getClass().getInterfaces());

        //2.得到参数是InvocationHandle的构造器
        Constructor<?> constructor = proxyClass.getConstructor(InvocationHandler.class);

        //3. 根据构造器得到代理对象:有参构造
        Object proxy = constructor.newInstance(new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                //4.增强该类的所有方法
                System.out.println("打印日志...");
                Object result = method.invoke(target, args);
                System.out.println("结束打印...");
                return result;
            }
        });
        return proxy;
    }

4.InvocationHandler接口

  • 1.上面说到Proxy.getProxyClass()得到的一个代理类的Class对象。而且这个Class对象中有一个有参构造方法对象。其中的参数是InvocationHandler接口。
  • 2.InvocationHandler接口有一个invoke()方法,里面的参数是Object proxy:是代理对象本身,Method method:方法对象,Object[] args参数数组。代理对象调用任何方法都会被invoke拦截,所以后面代理对象想要实现什么额外的功能,都可以放到重写的invoke()方法里面。
    public static Object generateProxy(Object target) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        //1.得到代理类Proxy的Class类对象:有InvocationHandle属性。Proxy类中有这个属性
        Class<?> proxyClass = Proxy.getProxyClass(target.getClass().getClassLoader(), target.getClass().getInterfaces());

        //2.得到参数是InvocationHandle的构造器
        Constructor<?> constructor = proxyClass.getConstructor(InvocationHandler.class);

        //3. 根据构造器得到代理对象:有参构造
        Object proxy = constructor.newInstance(new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                //4.增强该类的所有方法
                System.out.println("打印日志...");
                Object result = method.invoke(target, args);
                System.out.println("结束打印...");
                return result;
            }
        });
        return proxy;
    }

5.动态代理的缺陷

动态代理生成的代理对象,最终都可以用接口接收,和目标对象一起形成了多态。

动态代理的目标对象必须有实现的接口。

得到的代理独享只能使用或增强继承的接口中定义的方法。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值