Java的静态代理、JDK动态代理与CGlib动态代理

154 阅读13分钟

1. 代理简介

代理模式是一种设计模式,提供了对目标对象额外的访问方式,即通过代理对象访问目标对象,这样可以在不修改原目标对象的前提下,提供额外的功能操作,扩展目标对象的功能。

简言之,代理模式就是设置一个中间代理来控制访问原目标对象,以达到增强原对象的功能和简化访问方式。

代理模式UML图

1.1 代理模式所涉及的4种角色

image.png

  • 主题接口:定义代理类和真实主题的公共对外方法,也是代理类代理真实主题的方法;
  • 真实主题:真正实现业务逻辑的类;
  • 客户端:使用代理类和主题接口完成一些工作。
  • 代理类:用来代理和封装真实主题;

在代理模式中真实主题角色对于客户端角色来说的透明的,也就是客户端不知道也无需知道真实主题的存在。 为了保持行为的一致性,代理类和委托类通常会实现相同的接口,所以在访问者看来两者没有丝毫的区别。通过代理类这中间一层,能有效控制对委托类对象的直接访问,也可以很好地隐藏和保护委托类对象,同时也为实施不同控制策略预留了空间,从而在设计上获得了更大的灵活性。

举个例子,我们生活中经常到火车站去买车票,但是人一多的话,就会非常拥挤,于是就有了代售点,我们能从代售点买车票了。这其中就是代理模式的体现,代售点代理了火车站对象,提供购买车票的方法。

1.2 代理的优点

  • 隐藏委托类的实现,调用者只需要和代理类进行交互即可。
  • 解耦,在不改变委托类代码情况下做一些额外处理,比如添加初始判断及其他公共操作

1.3 代理的应用场景

代理的使用场景很多,struts2中的 action 调用, hibernate的懒加载, spring的 AOP无一不用到代理。总结起来可分为以下几类:

  • 在原方法执行之前和之后做一些操作,可以用代理来实现(比如记录Log,做事务控制等)。
  • 封装真实的主题类,将真实的业务逻辑隐藏,只暴露给调用者公共的主题接口。
  • 在延迟加载上的应用。

2. 静态代理

所谓静态代理也就是在程序运行前就已经存在代理类的字节码文件,代理类和委托类的关系在运行前就确定了。
下面有个场景,一个房主要出售自己的房子,但房主不知道谁要买房,也没有时间去接待每一个看房者。
现在我们就用静态代理的方式来实现房主的这一需求。
首先,将出售房屋抽象成公共代理接口(Sales)

public interface Sales { 
    void sell(); 
}

Salese接口里面提供了sell方法表示出售房屋。

其次,房主做为整个事件的主角,自然而然的就成了真实主题,也是代理的委托者

/** * 真实主题,具体处理业务 */ 
public class Owner implements Sales{ 
    @Override 
    public void sell() { 
        System.out.println("我是房东我正在出售我的房产"); 
    } 
}

真实主题Owner实现了Sales接口,在接口提供的sell()方法里出售自己的房屋。

再次,给房主找个中介(Agents),作为房主出售房屋的代理

/** * 代理类:用来代理和封装真实主题<br> * 在这里表示房产中介 * */ 
public class Agents implements Sales { 
    private Owner owner; 
    
    public Agents() {} 
    @Override 
    public void sell() { 
        System.out.println("我是房产中介,正在核实买房者是否符合购买该房屋的资格"); 
        getOwner().sell(); 
        System.out.println("我是房产中介,正在收取提成"); 
    } 
    
    private Owner getOwner() { 
        if (owner==null) { 
            owner=new Owner(); 
        }
        return owner; 
    } 
}

为了帮房主出售房屋,Agents代理也实现了Sales接口。同时Agents也拥有Owner的成员对象,在实现的sell()接口方法里,代理Agents帮房主Owner预处理了一些消息,然后调用了owner对象的sell()方法通知房主出售房屋,在房主Owner出售完房屋后,代理Agents开始收取中介费。有心的读者可以发现,代理Agents在访问Owner对象的时候使用了getOwner()方法,从而达到了在客户真正决定买房的时候才初始化owner对象,而不是在Agents初始化的时候就将Owner初始化。真实情境中,会有很多购房者要看房,但真正买的人只有一个,所以在代理Agents帮房东预处理和过滤掉所有信息之后,告诉房东你可以出售房屋了,这样大大节省了房东的时间和简化了售房的繁琐过程。而这也是用代理来实现延迟加载的好处。

顾客(Customer)

/** * 客户端,使用代理类和主题接口完成一些工作。 这里表示顾客 */ 
public class Customer { 
    public static void main(String[]args) { 
        Sales sales=new Agents();
        sales.sell(); 
    } 
}

在这里买房的顾客Customer找到房产代理Agents,告诉他要买房。整个过程房东Owner对顾客Customer来说是透明的,Customer只与Agents打交道,Owner也只与Agents打交道,Agents作为Owner和Customer通信的桥梁从而有效控制了Customer和Owner的直接交流。

观察代码可以发现每一个代理类只能为一个接口服务,这样一来程序开发中必然会产生过多的代理,而且,所有的代理操作除了调用的方法不一样之外,其他的操作都一样,则此时肯定是重复代码。解决这一问题最好的做法是可以通过一个代理类完成全部的代理功能,那么此时就必须使用动态代理完成。

2.1 静态代理的优缺点

优点:业务类只需要关注业务逻辑本身,保证了业务类的重用性。这是代理的共有优点。
缺点:
1)代理对象的一个接口只服务于一种类型的对象,如果要代理的方法很多,势必要为每一种方法都进行代理,静态代理在程序规模稍大时就无法胜任了。
2)如果接口增加一个方法,除了所有实现类需要实现这个方法外,所有代理类也需要实现此方法。增加了代码维护的复杂度。
另外,如果要按照上述的方法使用代理模式,那么真实角色(委托类)必须是事先已经存在的,并将其作为代理对象的内部属性。但是实际使用时,一个真实角色必须对应一个代理角色,如果大量使用会导致类的急剧膨胀;此外,如果事先并不知道真实角色(委托类),该如何使用代理呢?这个问题可以通过Java的动态代理类来解决。

3. 动态代理

3.1 JDK动态代理

JDK动态代理

利用反射机制生成一个实现代理接口的匿名类,在调用具体方法前调用InvokeHandler来处理。

在JDK的动态代理API中,有两个重要的类和接口,一个是 InvocationHandler(Interface)、另一个则是 Proxy(Class),这一个类和接口是实现我们动态代理所必须用到的。

InvocationHandler(Interface)

InvocationHandler是负责连接代理类和委托类的中间类必须实现的接口,它自定义了一个 invoke 方法,用于集中处理在动态代理类对象上的方法调用,通常在该方法中实现对委托类的代理访问。

InvocationHandler 的核心方法

Object invoke(Object proxy, Method method, Object[] args)

  • proxy 该参数为代理类的实例
  • method 被调用的方法对象
  • args 调用method对象的方法参数

该方法也是InvocationHandler接口所定义的唯一的一个方法,该方法负责集中处理动态代理类上的所有方法的调用。调用处理器根据这三个参数进行预处理或分派到委托类实例上执行。

Proxy(Class)

Proxy是 Java 动态代理机制的主类,它提供了一组静态方法来为一组接口动态地生成代理类及其对象。
Proxy 的静态方法

static InvocationHandler getInvocationHandler(Object proxy)
该方法用于获取指定代理对象所关联的调用处理器
static Class getProxyClass(ClassLoader loader, Class[] interfaces)
该方法用于获取关联于指定类装载器和一组接口的动态代理类的类对象
static boolean isProxyClass(Class cl)
该方法用于判断指定类对象是否是一个动态代理类 static Object newProxyInstance(ClassLoader loader, Class[] interfaces,InvocationHandler h)

  • loader 指定代理类的ClassLoader加载器
  • interfaces 指定代理类要实现的接口
  • h: 表示的是当我这个动态代理对象在调用方法的时候,会关联到哪一个InvocationHandler对象上

该方法用于为指定类装载器、一组接口及调用处理器生成动态代理类实例

3.1.1 实例

基于接口的动态代理

  • 涉及的类:Proxy
  • 提供者:JDK官方
  • 如何创建代理对象:使用Proxy类中的newProxyInstance方法
  • 创建代理对象的要求:被代理类最少实现一个接口,如果没有则不能使用
  • newProxyInstance方法的参数:
    • ClassLoader:类加载器,它是用于加载代理对象字节码的。和被代理对象使用相同的类加载器。固定写法。
    • Class[]:字节码数组,它是用于让代理对象和被代理对象有相同方法。固定写法。
    • InvocationHandler:用于提供增强的代码,它是让我们写如何代理。我们一般都是些一个该接口的实现类,通常情况下都是匿名内部类

目标接口

public interface UserManager {
    void addUser(String username, String password);
    void delUser(String username);
}

接口实现类

public class JDKProxy implements InvocationHandler {
    // 用于指向被代理对象
    private Object targetObject;
    public Object newProxy(Object targetObject) {
        // 将被代理对象传入进行代理
        this.targetObject = targetObject;
        // 返回代理对象
        return Proxy.newProxyInstance(this.targetObject.getClass().getClassLoader(),this.targetObject.getClass().getInterfaces(),this);
    }

    /**
     * 被代理对象的任何方法执行时,都会被invoke方法替换,即:代理对象执行被代理对象中的任何方法时,实际上执行的时当前的invoke方法
     * @param proxy(代理对象的引用)
     * @param method(当前执行的方法)
     * @param args(当前执行方法所需的参数)
     * @return(和被代理对象方法有相同的返回值)
     * @throws Throwable
     */
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 在原来的方法上增加了日志打印功能,增强代码
        printLog();
        Object ret = null;
        // 调用invoke方法(即执行了代理对象调用被调用对象中的某个方法)
        ret = method.invoke(targetObject, args);
        return ret;
    }

    /**
     * 模拟日志打印
     */
    private void printLog() {
        System.out.println("日志打印:printLog()");
    }
}

测试类

public class TestJDKProxy {
    public static void main(String[] args) {
        UserManager userManager = new UserManagerImpl();
        JDKProxy jdkProxy = new JDKProxy();
        UserManager userManagerProxy = (UserManager)jdkProxy.newProxy(userManager);
        System.out.println("--------------------没有使用增强过的方法--------------------");
        userManager.addUser("root","root");
        userManager.delUser("root");
        System.out.println("--------------------使用代理对象增强过的方法--------------------");
        userManagerProxy.addUser("scott","tiger");
        userManagerProxy.delUser("scott");
    }
}

测试结果

--------------------没有使用增强过的方法--------------------
调用了UserManagerImpl.addUser()方法!
调用了UserManagerImpl.delUser()方法!
--------------------使用代理对象增强过的方法--------------------
日志打印:printLog()
调用了UserManagerImpl.addUser()方法!
日志打印:printLog()
调用了UserManagerImpl.delUser()方法!

3.2 CGlib动态代理

CGlib动态代理

利用ASM(开源的Java字节码编辑库,操作字节码)开源包,将代理对象类的class文件加载进来,通过修改其字节码生成子类来处理。

3.2.1 实例

基于子类的动态代理:

  • 涉及的类:Enhancer
  • 提供者:第三方cglib库
  • 如何创建代理对象:使用Enhancer类中的create方法
  • 创建代理对象的要求:被代理类不能是final类
  • create方法的参数:
    • Class:字节码,它是用于指定被代理对象的字节码。固定写法。
    • Callback():用于提供增强的代码,它是让我们写如何代理。我们一般都是些一个该接口的实现类。固定写法。
public class CGLibProxy implements MethodInterceptor {
    // 用于指向被代理对象
    private Object targetObject;

    // 用于创建代理对象
    public Object createProxy(Object targetObject) {
        this.targetObject = targetObject;
        return new Enhancer().create(this.targetObject.getClass(),this);
    }

    /**
     * 
     * @param proxy(代理对象的引用)
     * @param method(当前执行的方法)
     * @param args(当前执行方法所需的参数)
     * @param methodProxy(当前执行方法的代理对象)
     * @return(和被代理对象方法有相同的返回值)
     * @throws Throwable
     */
    @Override
    public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
        Object ret = null;
        // 过滤方法
        if ("addUser".equals(method.getName())) {
            // 日志打印
            printLog();
        }
        ret = method.invoke(targetObject, args);
        return ret;
    }

    /**
     * 模拟日志打印
     */
    private void printLog() {
        System.out.println("日志打印:printLog()");
    }
}

测试类

public class TestCGLibProxy {
    public static void main(String[] args) {
        CGLibProxy cgLibProxy = new CGLibProxy();
        UserManager userManager = new UserManagerImpl();
        UserManager cgLibProxyProxy = (UserManager)cgLibProxy.createProxy(userManager);
        System.out.println("--------------------没有使用增强过的方法--------------------");
        userManager.addUser("root","root");
        userManager.delUser("root");
        System.out.println("--------------------使用代理对象增强过的方法--------------------");
        cgLibProxyProxy.addUser("scott","tiger");
        cgLibProxyProxy.delUser("scott");
    }
}

测试结果

--------------------没有使用增强过的方法--------------------
调用了UserManagerImpl.addUser()方法!
调用了UserManagerImpl.delUser()方法!
--------------------使用代理对象增强过的方法--------------------
日志打印:printLog()
调用了UserManagerImpl.addUser()方法!
调用了UserManagerImpl.delUser()方法!

3.3 动态代理类优缺点

优点

  • 动态代理类的字节码在程序运行时由Java反射机制动态生成,无需程序员手工编写它的源代码。
  • 动态代理类不仅简化了编程工作,而且提高了软件系统的可扩展性,因为Java 反射机制可以生成任意类型的动态代理类。

缺点

  • JDK的动态代理机制只能代理实现了接口的类,而不能实现接口的类就不能实现JDK的动态代理
  • cglib是针对类来实现代理的,他的原理是对指定的目标类生成一个子类,并覆盖其中方法实现增强,但因为采用的是继承,所以不能对final修饰的类进行代理。

3.4 JDK动态代理和CGlib动态代理的区别

  1. JDK代理使用的是反射机制实现aop的动态代理,CGLIB代理利用ASM开源包,对代理对象类的class文件加载进来,通过修改其字节码生成子类来处理。所以JDK动态代理的方式创建代理对象效率较高,执行效率较低,CGlib动态代理创建效率较低,执行效率高;

  2. JDK动态代理是面向接口的。 CGLib动态代理是通过字节码底层继承要代理类来实现,因此如果被代理类被final关键字所修饰,会失败。

  3. 使用注意: 如果要被代理的对象是个实现类,那么Spring会使用JDK动态代理来完成操作(Spirng默认采用JDK动态代理实现机制); 如果要被代理的对象不是个实现类那么,Spring会强制使用CGLib来实现动态代理。

JDK和CGLib动态代理性能对比

关于两者之间的性能的话,网上有人对于不通版本的jdk进行测试,经过多次试验,测试结果大致是这样的,在1.6和1.7的时候,JDK动态代理的速度要比CGLib动态代理的速度要慢,但是并没有教科书上的10倍差距,在JDK1.8的时候,JDK动态代理的速度已经比CGLib动态代理的速度快很多了,但是JDK动态代理和CGLIB动态代理的适用场景还是不一样的哈!