Java Proxy 动态代理

一、代理的概念与作用

1.1、生活中的代理

杭州人从在杭州本地从杭州的代理商(线下商店)中买联想电脑和直接跑到北京来联想总部买电脑。最终的主体业务目标有什么区别吗?基本上是一样的。但是,从代理商那里买就省去了直接跑北京去买电脑的成本和时间了。

1.2、程序中的代理

软件开发中也经常用到代理。程序中要为已存在的多个具有相同接口的目标类的各个方法增加一些系统功能,例如:异常处理,日志,计算方法的运行时间,事务管理,权限管理等等。

二、Java静态代理

代理图

客户端可以直接调用Target目标类,但是无法做到在程序之前处理相似的系统功能。所以客户端调用Proxy代理类,在Proxy代理类调用目标类的方法中间加入相似的系统功能就很方便。假如要统计一个类的某个方法的运行时间,这个类的源代码没有给你,你该怎么做?
编写一个与目标类具有相同的接口的代理类,代理类的每个方法调用目标类的相同方法,并在调用方法的时候附加上系统功能代码。

  • 目标类
public class Hello {
    public void sayHello(){
        System.out.println("Hello Java!");
    }
}
  • 自定义的代理类
public class HelloProxy {

    private Hello hello = new Hello();
    
    /**
     * 统计sayHello方法所用的时间
     */
    public void sayHello(){
        //前置功能代码
        long startTime = System.currentTimeMillis();
        //调用目标方法
        hello.sayHello();
        //后置功能代码
        long endTime = System.currentTimeMillis();
        System.out.println("Hello类的sayHello方法花费了:" + (endTime - startTime) + "毫秒");
    }
}

这样写的缺点显而易见,要为系统的各种接口的类增加代理功能,那将需要太多的代理类,全部采用静态代理方式 ,将是一件非常麻烦的事情,要写成百上千的代理类。使用静态代理实现的方式来增加系统功能是不可取的。

三、Java动态代理

动态代理

3.1、 JVM虚拟机生成Java动态代理类

JVM虚拟机可以在运行期动态生成出类的字节码,这种动态生成的类往往被用作代理类,即动态代理类。

JVM生成的动态类必须实现一个或多个接口,所以,JVM生成的动态类只能用作具有相同接口的目标代理类。问题来了,为什么JVM生成的动态类必须实现一个或多个接口呢?要想JVM生成一个代理类,那JVM必须需要知道什么呢?

  • JVM生成的类有什么方法呢?假如说一个类有一百个方法,要告诉JVM100个方法吗?显然不可能。
  • 直接告诉JVM一个接口,直接实现这个接口,不就等于告诉了所有方法了吗?JVM就知道了我要实现这个接口的所有方法。JVM生成动态类必须实现一个或多个接口,所以,JVM生成的动态类只能用作具有相同接口的目标类的代理。

下面就用程序实现创建动态类的实例对象及调用其方法:


Proxy类

要实现JavaJVM的动态代理技术,就要使用到java.lang.reflect.Proxy这个类。

动态生成类的字节码,并打印生成类的构造函数

    /**
     * 动态生成类的字节码,并打印生成类的构造函数
     */
    public static void getConStructors(){

        //动态生成代理类
        //ClassLoader:  每一个Class就必须有一个类加载器加载进来的,比如每个人都有一个妈妈。既然需要JVM动态生成Java类,所以要为动态生成类的字节码指定类加载器
        //Class Interfaces: 动态生成的字节码实现了哪些接口
        Class clazzProxy1 = Proxy.getProxyClass(Collection.class.getClassLoader(), Collection.class);

        //获取这个代理类的构造方法
        Constructor[] constructors = clazzProxy1.getConstructors();

        System.out.println("---------------------begin Construstors-----------------");
        //遍历构造方法
        for (Constructor constructor: constructors) {
            //获取每个名称
            String name = constructor.getName();
            StringBuilder sb = new StringBuilder(name);
            sb.append("(");
            //获取每个构造方法的参数类型
            Class[] clazzTypes = constructor.getParameterTypes();
            for (Class clazzType : clazzTypes) {
                sb.append(clazzType.getName()).append(".");
            }
            if(clazzTypes != null && clazzTypes.length != 0){
                sb.deleteCharAt(sb.length() - 1);
            }
            sb.append(")");
            System.out.println(sb.toString());
        }
    }
输出结果

通过打印构造方法,得到的动态代理类只有一个带参数的构造方法,而且是一个InvocationHandler参数,这个参数放入后面的内容讲解。

要通过Proxy类来动态生成代理类,就必须要传入两个参数
- 动态生成类的字节码指定类加载器
- 动态生成的字节码实现了哪些接口

动态生成类的字节码,并打印生成类的所有方法

   /**
     * 动态生成类的字节码,并打印动态类的每个方法
     */
    public static void getMethods(){
        //动态生成代理类
        Class clazzProxy1 = Proxy.getProxyClass(Collection.class.getClassLoader(), Collection.class);

        //获取这个代理类的构造方法
        Method[] methods = clazzProxy1.getMethods();

        System.out.println("---------------------begin Construstors-----------------");
        //遍历构造方法
        for (Method method: methods) {
            //获取每个名称
            String name = method.getName();
            StringBuilder sb = new StringBuilder(name);
            sb.append("(");
            //获取每个构造方法的参数类型
            Class[] clazzTypes = method.getParameterTypes();
            for (Class clazzType : clazzTypes) {
                sb.append(clazzType.getName()).append(".");
            }
            if(clazzTypes != null && clazzTypes.length != 0){
                sb.deleteCharAt(sb.length() - 1);
            }
            sb.append(")");
            System.out.println(sb.toString());
        }
    }
输出结果

通过打印结果可知,动态类生成的每个方法都有Collection接口的每个方法和Object类的每个方法。

创建动态类的实例对象及调用其方法并实现InvocationHander接口

上面内容通过打印构造方法,得到的动态代理类只有一个带参数的构造方法,这个构造方法就是java.lang.reflect.InvocationHandler接口,InvocationHandler是一个接口,所以我们要手动写自己的实现类来实现这个接口。先不用管实现这个接口具体是做什么的。

InvocationHandler接口
   /**
     * 创建动态类的实例对象及调用其方法
     * @param args
     * @throws Exception
     */
    public static void main(String[] args) throws Exception{
        
        //通过打印构造方法,得到的动态代理类有一个InvocationHandler参数
        Class clazzProxy1 = Proxy.getProxyClass(Collection.class.getClassLoader(), Collection.class);
        //获取Constructor类
        Constructor constructor = clazzProxy1.getConstructor(InvocationHandler.class);
        //传递InvocationHandler参数,手动实现InvocationHander接口
        //返回的结果是Collection接口的对象
        Collection proxy1 = (Collection) constructor.newInstance(new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                
                return null;
            }
        });
        /**
         * 通过打印生成的对象发现结果为null 有两种种可能:
         * 第一种可能是对象为null
         * 第二种可能是对象的toString()方法为null
         */
        System.out.println(proxy1);
        //对象没有报空指针异常,所以对象的toString为null,可以得出结论,代理类对象的toString()方法被代理类重写了。
        System.out.println(proxy1.toString());
        //调用一个方法,运行成功,所以proxy1不为null
        proxy1.clear();
        
        //调用size方法出错,为什么出错呢?size方法是有返回值的。
        proxy1.size();
    }
输出结果

通过打印结果可知,说明动态代理类生成的对象proxy1不为null,而且proxy1对象可以调用没有返回值的方法,不能调用有返回值的方法。调用有返回值的方法会出现异常。为什么会出现异常,将会在后面讲解。

完成InvocationHandler对象的内部功能

java.lang.reflect.Proxy 类还为我们直接提供创建出代理对象的方式,就是调用Proxy.newProxyInstance方法。就省去了先获取动态类的Class对象,再通过Class对象获取动态类的对象的过程了。

public static void getMyInstance(){
        //Proxy.newInstance方法直接创建出代理对象
        Collection proxy1 = (Collection) Proxy.newProxyInstance(
                Collection.class.getClassLoader(), 
                new Class[]{Collection.class},
                new InvocationHandler() {
                    //方法外部指定目标
                    List target = new ArrayList<>();
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        //在调用代码之前加入系统功能代码
                        long startTime = System.currentTimeMillis();
                        //睡眠1秒钟
                        Thread.sleep(1000);
                        //目标方法
                        Object retVal = method.invoke(target, args);
                        //在调用代码之后加入系统功能代码
                        long endTime = System.currentTimeMillis();
                        System.out.println( method.getName() + "方法花费了:" + (endTime - startTime) + "毫秒");
                        return retVal;
                    }
                });

        proxy1.add("a");
        proxy1.add("b");
        proxy1.add("c");
        //3
        System.out.println(proxy1.size());
    }


输出结果

通过打印结果可知,其实每次调用代理对象的每个方法,都会调用InvocationHandler的invoke方法。那为什么会每次调用代理对象的每个方法,会调用我们实现的InvocationHandler实现类的invoke方法呢?它的内部结构是怎么样的呢。现在就模拟一下调用流程。否则知其然就不知其所以然了。构造方法接受了InvocationHandler对象是为了干什么,内部的实现方式是什么样子的。我们了解,只要构造方法只要接受参数对象,就是是为了以后要一定要用到这个参数对象。

  • 模拟一下通过Proxy.newProxyInstance()方法生成的代理类的内部是大概是什么样子的。

import java.lang.reflect.Method;
// 模拟MyInvocationHandler对象
interface MyInvocationHandler {

    public Object invoke(Object target,Method method,Object[] args);
    
}

/**
 * 模拟JVM生成的代理类的内部大致结构
 * 并介绍之前使用的size()和add()方法
 * 因为实现了Connection接口,所以生成的代理类的方法就是Connection接口的方法
 */
public class Proxy1 {

    //自定义的MyInvocationHandler接口接受三个对象
    MyInvocationHandler handler;
    
    
    //代理类的内部方法

    //每调用一下,invoke方法就执行一次
    Object size() throws Exception{
        //内部调用InvocationHandler的invoke方法,而InvocationHandler是一个接口,所以 handler.invoke()方法是调用了我们的实现类的invoke()方法。
        //proxy对象,size方法,参数
        return handler.invoke(this,this.getClass().getMethod("size", null), null);
    }
    
    
    //每调用一下,invoke方法就执行一次
    Object add(Object args) throws Exception{
        //内部调用InvocationHandler的invoke方法
        //proxy对象,size方法,参数
        return handler.invoke(this,this.getClass().getMethod("size", null), new Object[]{args});
    }
    
}

调用流程
  1. 通过JVM动态生成的代理类Proxy1生成代理对象,并传入InvocationHandler实例对象。
  2. 当代理对象调用size()方法的时候,就调用了InvocationHandler接口里的invoke方法。
  3. 而InvocationHandler的实现类是我们自定义的,所以就调用了我们实现类里的invoke()方法,并把proxy代理对象和调用的Method方法对象和方法参数对象传递了进来。
  4. 然后在invoke方法内部通过Method.invoke()方法修改目标对象ArrayList实例来调用。
动态代理

通过上面模拟的代码和调用流程图就不难知道为什么会每次调用代理对象的每个方法,会调用我们实现的InvocationHandler实现类的invoke方法。还有为什么调用有返回值的方法会出现异常也不难而知了,因为invoke方法如果不做处理直接返回null的话,就会出现异常。

总结

1. 生成的类中有你那些方法,通过让其实现的接口告诉类加载器。
2. 产生的类中的字节码必须有一个关联类加载器对象。
3. 生成的类中的方法的代码是怎么样的,也由我们提供。把我们的代码写在一个约定好的接口对象的方法中,把对象传给它,它调用我的方法,即相当于插入了我们的代码。提供执行代码的对象就是那个InvocationHandler对象,它是在创建动态类实例对象的构造方法中传递进去的,在上面的InvocationHandler对象的Invoke方法加一点代码,就可以看到这些代码被调用运行了。

3.2、 CGLIB库生成Java动态代理类

如果有一个目标类,这个目标类本身没有实现接口。那通过什么样的方式来生成代理类呢?那JVM就无法生成代理类了。CGLIB可以动态生成一个类的子类,一个类的子类也可以用作该类的代理,所以,如果要为一个没有实现接口的类生成动态代理类,那么可以使用CGLIB库。

3.3、 代理类增加系统功能插入方法的位置

通过上面代理类的介绍,代理类的各个方法中通常除了要调用目标的相应方法和对外返回目标的返回结果外,还可以在代理方法中如下四个位置加上系统功能代码:
- 在调用目标方法之前
- 在调用目标方法之后
- 在调用目标方法的前后
- 在处理目标方法异常的catch块中

3.4、 编写可生成代理和插入通告的通用方法

介绍完动态代理的原理之后,我们思考一下上面的代码设计有非常明显的不足,我们自定义拦截逻辑的系统功能和目标对象被硬编码写死在代理对象中。我们要把代码以参数对象的形式封装传递进来。以参数对象的形式传递进来的好处是,在系统运行的时候,可以临时的灵活加入系统功能,实现系统功能灵活解耦。要为InvocationHandler传递两个对象:
- 目标对象target
- 系统功能对象

import java.lang.reflect.Method;

/**
 * 实现系统功能接口
 * @author
 *
 */
public interface Advice {

    void beforeMethod(Method method);
    
    void afterMethod(Method method);

}

import java.lang.reflect.Method;
/**
 * 自定义系统功能的实现类
 * @author tianshuo
 *
 */
public class MyAdvice implements Advice {

    @Override
    public void beforeMethod(Method method) {
        System.out.println("在目标方法之前调用!");
    }

    @Override
    public void afterMethod(Method method) {
        System.out.println("在目标方法之后调用!");
    }

}

   /**
     * 使用传递参数的方式灵活创建代理对象
     * @param target:目标对象
     * @param advice:系统功能对象
     * @return Proxy Object:代理对象
     */
    public static Object getProxy(final Object target,final Advice advice){

        //Proxy.newInstance方法直接创建出代理对象
        Object proxy3 = Proxy.newProxyInstance(
                target.getClass().getClassLoader(), 
                target.getClass().getInterfaces(),
                new InvocationHandler() {
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

                        advice.beforeMethod(method);

                        Object retVal = method.invoke(target, args);

                        advice.afterMethod(method);

                        return retVal;
                    }
                });
        return proxy3;
    }
    /**
     * 调用代理对象
     * @param args
     * @throws Exception
     */
    public static void main(String[] args) throws Exception{
        List target = new ArrayList<>();
        List proxyObject = (List) getProxy(target, new MyAdvice());
        proxyObject.add("abc");
        System.out.println(proxyObject.size());

    }   
输出结果

通过打印结果可知,说明我们实现传入参数方式来灵活实现代理功能的方式是正确的。

四、AOP 面向切面编程

系统中存在交叉业务,一个业务 就是要切入到系统的一个方面:

模块交叉业务

安全,事务,日志等功能要贯穿到好多个模块中,所以,它们就是交叉业务。

切面

上面这样的编程像是一个面一样,就是面向切面编程。AOP的目标就是使交叉业务模块化,可以采用将切面代码移动到原始代码周围,只写一份,其他的地方全部自动应用上,而不是在每个地方都写重复的代码。使用代理技术正好可以解决这种问题,因此代理是实现AOP功能的的核心和关键技术。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,214评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,307评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,543评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,221评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,224评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,007评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,313评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,956评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,441评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,925评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,018评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,685评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,234评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,240评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,464评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,467评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,762评论 2 345

推荐阅读更多精彩内容

  • 从三月份找实习到现在,面了一些公司,挂了不少,但最终还是拿到小米、百度、阿里、京东、新浪、CVTE、乐视家的研发岗...
    时芥蓝阅读 42,184评论 11 349
  • 这篇博客主要介绍使用 InvocationHandler 这个接口来达到 hook 系统 service ,从而实...
    Shawn_Dut阅读 5,033评论 0 33
  • 1、代理概念 为某个对象提供一个代理,以控制对这个对象的访问。 代理类和委托类有共同的父类或父接口,这样在任何使用...
    孔垂云阅读 7,598评论 4 54
  • 从今天起 要做一个 从上到下 由内至外都散发出 挣钱欲望的 美女子 因为昨天有人骂我“你怎么能没有挣钱的欲望!”
    敬千帆阅读 255评论 0 2
  • 姐姐家养了一只兔子。 很普通的品种。 但一家人把它宠得不像话。 姐夫回到家,第一件事就是深情呼唤兔子。 姐姐抱着兔...
    可可林阅读 340评论 0 0