关键词搜索

源码搜索 ×
×

一篇文章带你深入理解 Java 中的动态代理

发布2020-02-22浏览561次

详情内容

一、为什么要引入动态代理

已经学习过静态代理:静态代理

假设现在项目经理有一个需求:在项目现有所有类的方法前后打印日志

如何在不修改已有代码的前提下,完成这个需求?

我首先想到的是静态代理。具体做法是:

(1)为现有的每一个类都编写一个对应的代理类,并且让它实现和目标类相同的接口(假设都有)
在这里插入图片描述
(2)代理角色中需要引入真实角色,也就是:代理对象=增强代码+目标对象(原对象)
在这里插入图片描述
静态代理的缺陷:程序员要手动为每一个目标类编写对应的代理类。如果当前系统已经有成百上千个类,工作量太大了

所以,现在我们的努力方向是:如何少写或者不写代理类,却能完成代理功能?

二、动态代理

每一个代理类只能为一个接口服务,如果要想通过一个代理完成全部的代理功能,此时就必须使用动态代理功能。

动态代理实现的是方法的增强,这里之所以可以完成全部的代理功能,是因为这些需要代理的类需要增强的方法是相同的,比如小明买房需要个中介(中介相当于代理),小红,小花买房这个行为都可以交由一个同一个中介。

创建对象的过程:
在这里插入图片描述
换个思路:
在这里插入图片描述
所谓的Class对象,是Class类的实例,而Class类是描述所有类的,比如Person类,Student类
在这里插入图片描述
因为创建一个对象的过程:我们编写的class类被编译成字节码文件,字节码文件被类加载器进内存生成class对象,最后由这个class对象创建实例

所以,要创建一个实例,最关键的就是得到对应的Class对象

Class对象包含了一个类的所有信息,比如构造器、方法、字段等。

所以可以利用 反射 不写代理类,而直接得到代理Class对象,然后根据它创建代理实例

这种情况需要代理类和目标类理应实现同一组接口

之所以实现相同接口,是为了尽可能保证代理对象的内部结构和目标对象一致,这样我们对代理对象的操作最终都可以转移到目标对象身上,代理对象只需专注于增强代码的编写

在这里插入图片描述
所以,可以这样说:接口拥有代理对象和目标对象共同的类信息。所以,我们可以从接口那得到理应由代理类提供的信息。

这也是动态代理的约束:目标对象一定是要有接口的,没有接口就不能实现动态代理(当然也可以借助 cglib代理解决)

但是别忘了,接口是无法创建对象的,怎么办?

Java 中的实现动态代理机制解决了这个问题,需要 java.lang.reflect.InvocationHandler 接口和 java.lang.reflect.Proxy 类的支持。

Proxy有个静态方法:getProxyClass(ClassLoader, interfaces),只要你给它传入类加载器和一组接口,它就给你返回代理Class对象。

用通俗的话说,getProxyClass()这个方法,会从你传入的接口Class中,“拷贝”类结构信息到一个新的Class对象中,但新的Class对象带有构造器,是可以创建对象的。

也就是一旦明确接口,就可以通过接口的Class对象,创建一个代理Class,然后通过代理 Class 即可创建代理对象

Proxy.getProxyClass() 这个方法的本质就是:以Class造Class

静态代理:
在这里插入图片描述
动态代理:
在这里插入图片描述
具体的:
在这里插入图片描述

其实无论是静态代理还是动态代理本质都是最终生成代理对象,区别在于静态代理对象需要人手动生成,而动态代理对象是运行时,JDK通过反射动态生成的代理类最终构造的对象,JDK生成的类在加载到内存之后就删除了,所以看不到类文件

有了 Class 对象:
在这里插入图片描述
根据代理Class的构造器创建对象时,需要传入InvocationHandler。每次调用代理对象的方法,最终都会调用InvocationHandler的invoke()方法:
在这里插入图片描述
根据代理Class的构造器创建对象时,需要传入InvocationHandler。通过构造器传入一个引用,那么必然有个成员变量去接收。没错,代理对象的内部确实有个成员变量invocationHandler,而且代理对象的每个方法内部都会调用handler.invoke()!

InvocationHandler对象成了代理对象和目标对象的桥梁,不像静态代理这么直接。
在这里插入图片描述
大家仔细看上图右侧的动态代理,我在invocationHandler的invoke()方法中并没有写目标对象。因为一开始invocationHandler的invoke()里确实没有目标对象,需要我们手动new。
在这里插入图片描述
但这种写法不够优雅,属于硬编码。我这次代理A对象,下次想代理B对象还要进来改invoke()方法,太差劲了。改进一下,让调用者把目标对象作为参数传进来:

作者:bravo1988
链接:https://www.zhihu.com/question/20794107/answer/658139129
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

public class ProxyTest {
	public static void main(String[] args) throws Throwable {
		CalculatorImpl target = new CalculatorImpl();
                //传入目标对象
                //目的:1.根据它实现的接口生成代理对象 2.代理对象调用目标对象方法
		Calculator calculatorProxy = (Calculator) getProxy(target);
		calculatorProxy.add(1, 2);
		calculatorProxy.subtract(2, 1);
	}

	private static Object getProxy(final Object target) throws Exception {
		//参数1:随便找个类加载器给它, 参数2:目标对象实现的接口,让代理对象实现相同接口
		Class proxyClazz = Proxy.getProxyClass(target.getClass().getClassLoader(), target.getClass().getInterfaces());
		Constructor constructor = proxyClazz.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;
	}
}

    这样就非常灵活,非常优雅了。无论现在系统有多少类,只要你把实例传进来,getProxy()都能给你返回对应的代理对象。

    就这样,我们完美地跳过了代理类,直接创建了代理对象!

    不过实际编程中,一般不用getProxyClass(),而是使用Proxy类的另一个静态方法:Proxy.newProxyInstance(),直接返回代理实例,连中间得到代理Class对象的过程都帮你隐藏

    总结:
    在这里插入图片描述

    1. Proxy 类

    Proxy 类是专门完成代理的操作类,可以通过此类为一个或多个接口动态地生成实现类,Proxy 类提供了如下的操作方法:

    public static Object newProxyInstance(ClassLoader loader,
    Class<?>[] interfaces,InvocationHandler h) throws IllegalArgumentException
    
    • 1
    • 2

    通过 newProxyInstance() 方法可以动态地生成实现类:
    在这里插入图片描述

    取得类加载器:

    class Person{}
    public class Root{
        public static void main(String[] args) {
            Person stu = new Person();//实例化子类对象
            System.out.println("类加载器:" +
                    stu.getClass().getClassLoader().getClass().getName());
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    对于Proxy 类需要传入,类加载器,接口,InvocationHandler 接口的子类实例

    2. InvocationHandler 接口

    根据代理Class的构造器创建对象时,需要传入InvocationHandler。每次调用代理对象的方法,最终都会调用InvocationHandlerinvoke()方法:

    InvocationHandler 接口的定义:

    public interface InvocationHandler{
    	public Object invoke(Onject proxy,Method method,Object[] args)
    	throws Throwable
    }
    
    • 1
    • 2
    • 3
    • 4

    该接口定义了一个 invoke() 方法,含有三个参数:
    在这里插入图片描述
    invoke 的具体学习可参考:动态代理 InvocationHandler 和 Invoke

    三、示例操作

    如果要完成动态代理,首先要定义一个 InvocationHandler 接口的子类,以完成代理的具体操作。

    1. 定义 MyInvocationHandler 的类
    class MyInvocationHandler implements InvocationHandler{
        private Object obj;//真实主题
        public Object bind(Object obj){//绑定真题操作主题
            this.obj = obj;
            //取得代理对象
            return Proxy.newProxyInstance(obj.getClass().getClassLoader(),
                    obj.getClass().getInterfaces(),this);//this 指的是MyInvocationHandler 
        }
        //动态调用方法
        public Object invoke(Object proxy, Method method,Object[] args) throws Throwable{
        	System.out.println("打印日志 !!!")
            Object temp = method.invoke(this.obj,args);//调用方法,传入真实的主题和参数
            return temp;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    这里 MyInvocationHandler 类的 bind() 方法中接受被代理的真实主题实现,之后覆写 InvocationHandler 接口中的 invoke() 方法

    1. 定义接口
    interface Subject{//定义 Subject 接口
        public String say(String name,int age);//定义抽象方法 say
    }
    
    • 1
    • 2
    • 3
    1. 定义真实主题实现类
    //定义真实主题实现类
    class RealSubject implements Subject{//真实实现类
        public String say(String name,int age){//覆写 say() 方法
            return "姓名:" + name + ",年龄:" + age;//返回信息
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    这里定义了接口和真实主题类,这样在操作时直接将真实主题类的对象传入到 MyInvocationHandler 类的 bind() 方法中即可。

    1. 测试动态代理
    public class Test{
        public static void main(String[] args) {
            MyInvocationHandler handler = new MyInvocationHandler();//实例化代理操作类
            Subject sub = (Subject)handler.bind(new RealSubject());//绑定对象
            String info = sub.say("Java",30);//通过动态代理调用方法
            System.out.println(info);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    四、代理 Class 和代理对象

    在这里插入图片描述
    单从名字看,代理Class和Calculator的接口确实相去甚远,但是我们却能将代理对象赋值给接口类型:
    在这里插入图片描述
    千万别觉得名字奇怪,就怀疑它不能用接口接收,只要实现该接口就是该类型。

    代理对象的本质就是:和目标对象实现相同接口的实例。代理Class可以叫任何名字,whatever,只要它实现某个接口,就能成为该接口类型。

    在这里插入图片描述
    我写了一个MyProxy类,那么它的Class名字必然叫MyProxy。但这和能否赋值给接口没有任何关系。由于它实现了Serializable和Collection,所以myProxy(代理实例)同时是这两个接口的类型。

    五、cglib 代理

    参考:Java3y

    1. 介绍

    由于静态代理需要实现目标对象的相同接口,那么可能会导致代理类会非常非常多,不好维护,
    因此出现了动态代理

    动态代理也有个约束:目标对象一定是要有接口的,没有接口就不能实现动态代理,因此出现了cglib代理

    cglib代理也叫子类代理,从内存中构建出一个子类来扩展目标对象的功能

    CGLIB是一个强大的高性能的代码生成包,它可以在运行期扩展Java类与实现Java接口。它广泛的被许多AOP的框架使用,例如Spring AOP和dynaop,为他们提供方法的interception(拦截)

    2. 编写 cglib 代理

    (1)需要引入cglib – jar文件, 但是spring的核心包中已经包括了cglib功能,所以直接引入spring-core-3.2.5.jar即可。

    (2)引入功能包后,就可以在内存中动态构建子类

    (3)代理的类不能为final,否则报错【在内存中构建子类来做扩展,当然不能为final,有final就不能继承了】

    (4)目标对象的方法如果为final/static, 那么就不会被拦截,即不会执行目标对象额外的业务方法。

    //需要实现MethodInterceptor接口
    public class ProxyFactory implements MethodInterceptor{
    
        // 维护目标对象
        private Object target;
        public ProxyFactory(Object target){
            this.target = target;
        }
    
        // 给目标对象创建代理对象
        public Object getProxyInstance(){
            //1. 工具类
            Enhancer en = new Enhancer();
            /https://files.jxasp.com/image/2. 设置父类
            en.setSuperclass(target.getClass());
            //3. 设置回调函数
            en.setCallback(this);
            //4. 创建子类(代理对象)
            return en.create();
        }
    
        @Override
        public Object intercept(Object obj, Method method, Object[] args,
                MethodProxy proxy) throws Throwable {
    
            System.out.println("开始事务.....");
    
            // 执行目标对象的方法
            Object returnValue = method.invoke(target, args);
    
            System.out.println("提交事务.....");
    
            return returnValue;
        }
    }
    
      33
    • 34
    • 35

    测试:

    public class App {
    
        public static void main(String[] args) {
            UserDao userDao = new UserDao();
            UserDao factory = (UserDao) new ProxyFactory(userDao).getProxyInstance();
            factory.save();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    在这里插入图片描述
    在这里插入图片描述
    使用 cglib 主要就是为了弥补动态代理的不足【动态代理的目标对象一定要实现接口】

    六、面试题

    【1】CGLIB动态代理的使用步骤是什么?

    相关技术文章

    点击QQ咨询
    开通会员
    返回顶部
    ×
    微信扫码支付
    微信扫码支付
    确定支付下载
    请使用微信描二维码支付
    ×

    提示信息

    ×

    选择支付方式

    • 微信支付
    • 支付宝付款
    确定支付下载